init: cobol-java migration verification platform v3 (42 tests, JCL module)

This commit is contained in:
hangshuo652
2026-05-27 08:42:41 +08:00
parent faeedbc77b
commit 7fcdb41a85
21 changed files with 870 additions and 148 deletions
+40
View File
@@ -0,0 +1,40 @@
# DESIGN.md — verify-cli Web UI
## Aesthetic
Terminal Developer Tool — 命令行工具的可视化包装。暗底、等宽、最小装饰。
用户的第一反应应该是"这是我的终端,只不过多了个表单"。
## Typography
- Primary: SF Mono / Fira Code / Cascadia Code / Consolas (等宽字体堆栈)
- 全部使用系统原生字体,零外部依赖
- 层级差异通过字号和颜色区分,不换字体家族
## Color
| Token | Hex | Usage |
|-------|-----|-------|
| bg | #0a0e14 | 页面底色 |
| panel | #12171f | 卡片/表单容器 |
| border | #1f2937 | 分割线 |
| text | #b2becd | 正文 |
| dim | #5c6e80 | 标签/次要信息 |
| accent | #39bae6 | 链接/操作色 |
| green | #7fd962 | 成功状态 |
| red | #f26d78 | 错误状态 |
| yellow | #ffad66 | 等待中状态 |
## Layout
- 单栏,最大宽度 680px
- Header → Sections → Footer 的垂直流
- 表单使用 CSS Grid 双列布局
- 结果页 Section 分离 Summary 和 Field Results
## Spacing
- 页面 padding: 3rem 1.5rem
- Section 内 padding: 1.5rem, 间距 1rem
- 表单 label 间距: .75rem
## Decisions
- 不使用 Jinja23.1+ 与 Starlette 不兼容),改用字符串替换
- 不使用任何 CSS 框架,纯手写 CSS variables
- 不使用 emoji 或装饰图标,状态通过颜色边框表达
- 无紫色渐变、无 3 列 icon grid、无居中布局、无装饰性波浪 — AI slop 反模式检查通过
-48
View File
@@ -1,48 +0,0 @@
from dataclasses import dataclass, field
from pathlib import Path
@dataclass
class Config:
project_name: str = ""
copybook_paths: list = field(default_factory=lambda: ["./copybooks"])
dialect: str = "ibm"
llm_model: str = "gpt-4o-mini"
llm_timeout: int = 15
llm_cache_dir: str = ".cache/llm"
coverage_default: str = "boundary"
rounding_mode: str = "TRUNCATE"
tolerance: float = 0.01
runner_mode: str = "native"
spark_master: str = "local[*]"
spark_input_format: str = "json"
num_records: int = 1000
branch_pass: float = 0.80
max_llm_cost: float = 0.50
@classmethod
def from_toml(cls, path="aurak.toml"):
import tomllib
try:
with open(path, "rb") as f:
d = tomllib.load(f)
except:
return cls()
c = cls()
p = d.get("project", {})
c.project_name = p.get("name", "")
c.copybook_paths = p.get("copybook_paths", c.copybook_paths)
c.dialect = p.get("dialect", "ibm")
ll = d.get("llm", {})
c.llm_model = ll.get("model", c.llm_model)
co = d.get("coverage", {})
c.coverage_default = co.get("default_target", "boundary")
cp = d.get("comparison", {})
c.rounding_mode = cp.get("rounding_mode", "TRUNCATE")
c.tolerance = cp.get("default_tolerance", c.tolerance)
r = d.get("runner", {})
c.runner_mode = r.get("mode", "native")
s = d.get("spark", {})
c.spark_master = s.get("master", "local[*]")
c.num_records = s.get("num_records", c.num_records)
return c
+49
View File
@@ -0,0 +1,49 @@
from dataclasses import dataclass, field
from pathlib import Path
from .mapping import MappingConfig, FieldMapping
@dataclass
class Config:
project_name: str = ""
copybook_paths: list = field(default_factory=lambda: ["./copybooks"])
dialect: str = "ibm"
llm_model: str = "gpt-4o-mini"
llm_timeout: int = 15
llm_cache_dir: str = ".cache/llm"
coverage_default: str = "boundary"
rounding_mode: str = "TRUNCATE"
tolerance: float = 0.01
runner_mode: str = "native"
spark_master: str = "local[*]"
spark_input_format: str = "json"
num_records: int = 1000
branch_pass: float = 0.80
max_llm_cost: float = 0.50
@classmethod
def from_toml(cls, path="aurak.toml"):
import tomllib
try:
with open(path, "rb") as f:
d = tomllib.load(f)
except:
return cls()
c = cls()
p = d.get("project", {})
c.project_name = p.get("name", "")
c.copybook_paths = p.get("copybook_paths", c.copybook_paths)
c.dialect = p.get("dialect", "ibm")
ll = d.get("llm", {})
c.llm_model = ll.get("model", c.llm_model)
co = d.get("coverage", {})
c.coverage_default = co.get("default_target", "boundary")
cp = d.get("comparison", {})
c.rounding_mode = cp.get("rounding_mode", "TRUNCATE")
c.tolerance = cp.get("default_tolerance", c.tolerance)
r = d.get("runner", {})
c.runner_mode = r.get("mode", "native")
s = d.get("spark", {})
c.spark_master = s.get("master", "local[*]")
c.num_records = s.get("num_records", c.num_records)
return c
+1
View File
@@ -30,6 +30,7 @@ class VerificationRun:
branch_rate: float = 0.0 branch_rate: float = 0.0
llm_cost: float = 0.0 llm_cost: float = 0.0
report_path: str = "" report_path: str = ""
debug: dict = field(default_factory=dict)
def __post_init__(self): def __post_init__(self):
if not self.timestamp: if not self.timestamp:
View File
+101
View File
@@ -0,0 +1,101 @@
"""JCL Executor — executes parsed JCL steps using platform components."""
from pathlib import Path
from jcl.parser import Job, JobStep, CondParam, COND_OPS
class JclExecutor:
"""Executes JCL Job steps. Uses platform CobolRunner for COBOL programs."""
def __init__(self, root_dir: str, cobol_dir: str, copybook_dir: str):
self.root_dir = Path(root_dir)
self.cobol_dir = Path(cobol_dir)
self.copybook_dir = Path(copybook_dir)
self.bin_dir = self.root_dir / "bin"
self.bin_dir.mkdir(exist_ok=True)
self.last_rc: int = 0
self.step_rcs: dict[str, int] = {}
self.results: dict[str, dict] = {}
def run(self, job: Job) -> int:
for step in job.steps:
rc = self._execute_step(step)
self.last_rc = rc
self.step_rcs[step.step_name] = rc
return self.last_rc
def _execute_step(self, step: JobStep) -> int:
# COND check
if step.cond and not self._check_cond(step.cond):
self.results[step.step_name] = {"status": "SKIPPED", "cond": str(step.cond)}
return 0
prog = step.program.upper()
if prog == "SORT":
return self._run_sort(step)
# Use platform's CobolRunner
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
from runners.cobol_runner import CobolRunner
cbl_path = self.cobol_dir / f"{step.program}.cbl"
runner = CobolRunner()
build = runner.compile(str(cbl_path))
if not build.success:
self.results[step.step_name] = {"status": "COMPILE_ERROR", "log": build.log[-200:]}
return 8
# Map DD entries to environment variables
env_in = {}
env_out = {}
for dd in step.dd_entries:
name = dd.dd_name.upper()
if dd.dsn:
path = self._resolve_path(dd.dsn)
if name in ("TRANSIN", "MEMBER", "VALIDIN", "RATE", "BILLING"):
env_in[name] = str(path)
elif name in ("VALIDOUT", "REJECT", "REPORTERR", "CALCOUT", "STMT", "SUMMARY"):
env_out[name] = str(path)
input_path = env_in.get(list(env_in.keys())[0], "")
output_path = env_out.get(list(env_out.keys())[0], str(self.root_dir / "data" / "work" / f"{step.step_name.lower()}_out.bin"))
run = runner.run(build.artifact_path, input_path, output_path)
self.results[step.step_name] = {
"status": "OK" if run.success else "ERROR",
"rc": 0 if run.success else 12
}
return 0 if run.success else 12
def _check_cond(self, cond: CondParam) -> bool:
if cond.step_name:
target = self.step_rcs.get(cond.step_name, 0)
return not COND_OPS.get(cond.operator, lambda x, y: False)(target, cond.code)
return True
def _run_sort(self, step: JobStep) -> int:
import subprocess
infile = None
outfile = None
for dd in step.dd_entries:
n = dd.dd_name.upper()
if n == "SORTIN" and dd.dsn:
infile = self._resolve_path(dd.dsn)
elif n == "SORTOUT" and dd.dsn:
outfile = self._resolve_path(dd.dsn)
if not infile or not outfile:
return 12
if infile.exists():
lines = sorted(infile.read_text().splitlines())
outfile.write_text("\n".join(lines))
self.results[step.step_name] = {"status": "OK", "rc": 0}
return 0
def _resolve_path(self, dsn: str) -> Path:
import re
dsn = re.sub(r"\(\+?\d+\)", "", dsn).strip(".")
return (self.root_dir / dsn.lstrip("/")).resolve()
+118
View File
@@ -0,0 +1,118 @@
"""JCL Parser — Phase 2. Parses JCL scripts into structured Job objects."""
import re
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class DDEntry:
dd_name: str
dsn: Optional[str] = None
disp: Optional[str] = None
sysout: Optional[str] = None
inline_data: list[str] = field(default_factory=list)
@dataclass
class CondParam:
code: int
operator: str # EQ, NE, GT, GE, LT, LE
step_name: Optional[str] = None
@dataclass
class JobStep:
step_name: str
program: str
dd_entries: list[DDEntry] = field(default_factory=list)
cond: Optional[CondParam] = None
parm: Optional[str] = None
@dataclass
class Job:
job_name: str
steps: list[JobStep] = field(default_factory=list)
COND_OPS = {
"EQ": lambda rc, code: rc == code, "NE": lambda rc, code: rc != code,
"GT": lambda rc, code: rc > code, "GE": lambda rc, code: rc >= code,
"LT": lambda rc, code: rc < code, "LE": lambda rc, code: rc <= code,
}
def parse_jcl(filepath: str) -> Optional[Job]:
"""Parse a JCL file into a Job object."""
with open(filepath, "r", encoding="utf-8") as f:
lines = _merge_continuations(f.readlines())
job = None
current_step: Optional[JobStep] = None
in_sysin = False
sysin_lines: list[str] = []
for line in lines:
stripped = line.strip()
if stripped.startswith("//*") or not stripped:
continue
if in_sysin:
if stripped == "/*":
if current_step and current_step.dd_entries:
current_step.dd_entries[-1].inline_data = sysin_lines
sysin_lines = []; in_sysin = False
else:
sysin_lines.append(stripped)
continue
if not stripped.startswith("//"):
continue
content = stripped[2:].strip()
# JOB
if re.search(r"\bJOB\b", content, re.IGNORECASE):
job = Job(job_name=content.split()[0])
continue
# EXEC
m = re.match(r"(\w+)\s+EXEC\s+(?:PGM=)?(\w+)", content, re.IGNORECASE)
if m:
step_name, program = m.group(1), m.group(2)
cond = None
cm = re.search(r"COND=\s*\(\s*(\d+)\s*,\s*(\w+)", content, re.IGNORECASE)
if cm:
cond = CondParam(code=int(cm.group(1)), operator=cm.group(2).upper())
current_step = JobStep(step_name=step_name, program=program, cond=cond)
if job:
job.steps.append(current_step)
continue
# DD
m = re.match(r"(\w+)\s+DD\s*(.*)", content, re.IGNORECASE)
if m and current_step is not None:
dd = DDEntry(dd_name=m.group(1))
params = m.group(2)
dm = re.search(r"DSN=\s*([^\s,]+)", params, re.IGNORECASE)
if dm:
dd.dsn = dm.group(1)
if dd.dd_name.upper() == "SYSIN" and "*" in params:
in_sysin = True
current_step.dd_entries.append(dd)
continue
return job
def _merge_continuations(lines: list[str]) -> list[str]:
merged, buf = [], ""
for line in lines:
s = line.rstrip("\n\r")
buf = (buf + s) if buf else s
if s.rstrip().endswith(",") and not s.strip().startswith("//*"):
continue
merged.append(buf); buf = ""
if buf:
merged.append(buf)
return merged
+16
View File
@@ -10,6 +10,7 @@ from runners.cobol_runner import CobolRunner
from runners.data_writer import DataWriter from runners.data_writer import DataWriter
from agents.agent1_parser import Agent1Parser from agents.agent1_parser import Agent1Parser
from agents.agent2_data import Agent2Data from agents.agent2_data import Agent2Data
from agents.agent3_diagnostic import Agent3Diagnostic
from agents.llm import LLMClient from agents.llm import LLMClient
from comparator.aligner import align_records from comparator.aligner import align_records
from comparator.field_compare import compare_field from comparator.field_compare import compare_field
@@ -31,6 +32,9 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
llm = LLMClient(model=cfg.llm_model, timeout=cfg.llm_timeout, cache_dir=cfg.llm_cache_dir) llm = LLMClient(model=cfg.llm_model, timeout=cfg.llm_timeout, cache_dir=cfg.llm_cache_dir)
tree = Agent1Parser(llm).parse(text) tree = Agent1Parser(llm).parse(text)
vr.llm_cost += 0.002 vr.llm_cost += 0.002
vr.debug["field_tree"] = [{"name":f.name,"level":f.level,"pic":f.pic,"usage":f.usage,
"offset":f.offset,"length":f.length,"redefines":f.redefines}
for f in tree.flatten().values()]
if not tree.fields: if not tree.fields:
return _done(vr, t0, "BLOCKED", 2) return _done(vr, t0, "BLOCKED", 2)
if vr.llm_cost > cfg.max_llm_cost: if vr.llm_cost > cfg.max_llm_cost:
@@ -38,6 +42,8 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
suite = Agent2Data(llm).design(tree, cfg.coverage_default, cfg.runner_mode == "spark") suite = Agent2Data(llm).design(tree, cfg.coverage_default, cfg.runner_mode == "spark")
vr.llm_cost += 0.002 vr.llm_cost += 0.002
vr.debug["test_cases"] = [{"id":tc.id,"fields":tc.fields,"targets":tc.coverage_targets} for tc in suite.test_cases]
vr.debug["spark_config"] = {"records":suite.spark_config.num_records} if suite.has_spark else None
bundle = TestDataBundle(base_path=Path("test-data-bundle")) bundle = TestDataBundle(base_path=Path("test-data-bundle"))
bundle.ensure_dirs() bundle.ensure_dirs()
@@ -51,6 +57,7 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
cob = CobolRunner() cob = CobolRunner()
build = cob.compile(cbl, cfg.dialect) build = cob.compile(cbl, cfg.dialect)
vr.debug["cobol_build"] = {"ok": build.success, "log": build.log[-300:]}
if not build.success: if not build.success:
return _done(vr, t0, "BLOCKED", 2) return _done(vr, t0, "BLOCKED", 2)
co = Path("cobol_out.bin") co = Path("cobol_out.bin")
@@ -61,6 +68,7 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
return _done(vr, t0, "BLOCKED", 2) return _done(vr, t0, "BLOCKED", 2)
runner: Runner = SparkJavaRunner(cfg.spark_master) if cfg.runner_mode == "spark" else NativeJavaRunner() runner: Runner = SparkJavaRunner(cfg.spark_master) if cfg.runner_mode == "spark" else NativeJavaRunner()
jb = runner.compile(java) jb = runner.compile(java)
vr.debug["java_build"] = {"ok": jb.success, "log": jb.log[-300:]}
if not jb.success: if not jb.success:
return _done(vr, t0, "BLOCKED", 2) return _done(vr, t0, "BLOCKED", 2)
inp = str(bundle.spark_input_dir() if cfg.runner_mode == "spark" else bundle.native_input()) inp = str(bundle.spark_input_dir() if cfg.runner_mode == "spark" else bundle.native_input())
@@ -95,6 +103,14 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
vr.status = "PASS" if m == 0 else "MISMATCH" vr.status = "PASS" if m == 0 else "MISMATCH"
vr.exit_code = 0 if m == 0 else 1 vr.exit_code = 0 if m == 0 else 1
diag = Agent3Diagnostic(llm)
for fr in frs:
if fr.status in ("MISMATCH", "NOT_SET", "NPE"):
try:
fr.suggestion = diag.analyze(fr) or ""
except:
pass
rd = Path(f"reports/{vr.program}") / vr.timestamp rd = Path(f"reports/{vr.program}") / vr.timestamp
rd.mkdir(parents=True, exist_ok=True) rd.mkdir(parents=True, exist_ok=True)
g = ReportGenerator() g = ReportGenerator()
+1
View File
@@ -0,0 +1 @@
{"id": "6aaa7e43", "status": "done", "copybook": "uploads\\6aaa7e43\\copybook.cpy", "cobol_src": "uploads\\6aaa7e43\\program.cbl", "java_src": "uploads\\6aaa7e43\\java", "mapping": "uploads\\6aaa7e43\\mapping.yaml", "runner": "native", "created": "2026-05-25T09:19:06.763502", "fields": [], "result": {"program": "java", "status": "ERROR", "matched": 0, "mismatched": 0, "duration": 34.83706521987915, "runner": "native"}}
+6 -3
View File
@@ -7,7 +7,10 @@
05 BR-STATUS PIC X. 05 BR-STATUS PIC X.
05 BR-DATE PIC 9(8). 05 BR-DATE PIC 9(8).
PROCEDURE DIVISION. PROCEDURE DIVISION.
DISPLAY BR-AMT. MOVE 1500 TO BR-AMT.
DISPLAY BR-STATUS. MOVE 'A' TO BR-STATUS.
DISPLAY BR-DATE. MOVE 20260522 TO BR-DATE.
DISPLAY BR-AMT
DISPLAY BR-STATUS
DISPLAY BR-DATE
STOP RUN. STOP RUN.
+226
View File
@@ -0,0 +1,226 @@
"""
Golden tests — 对接真实 COBOL 信用卡月结系统
数据源: D:\cobol-java\jcl-cobol-git\
验证目标: 28 transactions → 20 valid + 8 rejected → 6 cards → ¥48,250.20 total
"""
import sys, os
sys.path.insert(0, r"D:\cobol-java\v3-gstack-code-gen")
from pathlib import Path
from data.field_tree import Field, FieldTree
from comparator.cobol_binary_reader import CobolBinaryReader
from comparator.normalizer import Normalizer
from comparator.field_compare import compare_field
from comparator.aligner import align_records
from preprocessor import CopybookPreprocessor
GOLDEN = Path(r"D:\cobol-java\jcl-cobol-git")
# ── Test 1: TXCPY COPYBOOK 结构验证 ──
def test_txcpy_field_count():
"""TXCPY COPYBOOK defines 6 top-level fields."""
txcpy = FieldTree(fields=[
Field(name="TX-CARD-NO", level=5, pic="9(16)", usage="DISPLAY", offset=0, length=16),
Field(name="TX-DATE", level=5, pic="9(8)", usage="DISPLAY", offset=16, length=8),
Field(name="TX-TYPE", level=5, pic="X", usage="DISPLAY", offset=24, length=1),
Field(name="TX-AMOUNT", level=5, pic="S9(9)V99", usage="DISPLAY", offset=25, length=11),
Field(name="TX-CURRENCY", level=5, pic="X(3)", usage="DISPLAY", offset=36, length=3),
Field(name="TX-MERCHANT", level=5, pic="X(20)", usage="DISPLAY", offset=39, length=20),
], copybook_name="TXCPY")
flat = txcpy.flatten()
assert len(flat) == 6, f"Expected 6 fields, got {len(flat)}"
assert flat["TX-CARD-NO"].length == 16
assert flat["TX-AMOUNT"].length == 11
# ── Test 2: 二进制文件读取 ──
def test_read_transactions():
"""读取交易数据,验证第一条记录结构正确。"""
txcpy = FieldTree(fields=[
Field(name="TX-CARD-NO", level=5, pic="9(16)", usage="DISPLAY", offset=0, length=16),
Field(name="TX-DATE", level=5, pic="9(8)", usage="DISPLAY", offset=16, length=8),
Field(name="TX-TYPE", level=5, pic="X", usage="DISPLAY", offset=24, length=1),
Field(name="TX-AMOUNT", level=5, pic="S9(9)V99", usage="DISPLAY", offset=25, length=12),
Field(name="TX-CURRENCY", level=5, pic="X(3)", usage="DISPLAY", offset=37, length=3),
Field(name="TX-MERCHANT", level=5, pic="X(20)", usage="DISPLAY", offset=40, length=20),
], copybook_name="TXCPY")
reader = CobolBinaryReader()
records = reader.read(str(GOLDEN / "data/input/transactions.dat"), txcpy)
# 文件 2044 bytes, record size ≈ 73, 大约 28 条记录
assert len(records) >= 28, f"Expected >=28, got {len(records)}"
r0 = records[0]
assert r0["TX-CARD-NO"] == "6222021234567800"
assert r0["TX-DATE"] == "20260501"
assert r0["TX-TYPE"] == "P"
# ── Test 3: COMP-3 RATE 解析 ──
def test_comp3_rate():
"""验证利率表 COMP-3 编码。"""
n = Normalizer()
data = (GOLDEN / "data/input/rate.dat").read_bytes()
assert len(data) == 24, "2 records × 12 bytes"
# Record 1: Cash rate
assert n.normalize_comp3(data[1:4]) == "5" # 0.0005
assert chr(data[0]) == "C"
# Record 2: Overdue rate
assert n.normalize_comp3(data[13:16]) == "500" # 0.0500
assert chr(data[12]) == "O"
# ── Test 4: 管道输出一致性 ──
def test_pipeline_counts():
"""验证 28→20+8→6 的管道计数。"""
validated = len((GOLDEN / "data/work/validated_tx.dat").read_text().splitlines())
rejected = len((GOLDEN / "data/output/rejected_tx.dat").read_text().splitlines())
assert validated == 20, f"Expected 20 valid, got {validated}"
assert rejected == 8, f"Expected 8 rejected, got {rejected}"
def test_error_report_coverage():
"""验证全部 7 条校验规则被触发。"""
errors = (GOLDEN / "data/output/error_report.dat").read_text()
expected_errors = ["INVALID-CARD", "FROZEN-CARD", "INVALID-MERCHANT",
"INVALID-AMOUNT", "INVALID-REFUND", "OUT-OF-MONTH", "MEMBER-NOT-FOUND"]
for e in expected_errors:
assert e in errors, f"Missing error: {e}"
def test_grand_total():
"""验证全局合计金额。"""
summary = (GOLDEN / "data/output/summary_report.dat").read_text()
assert "AMOUNT: 48250.20" in summary
assert "INTEREST: 300.00" in summary
assert "FEE: 800.00" in summary
assert "CARDS:00006" in summary
# ── Test 5: COPY REPLACING 展开 ──
def test_copy_replacing_datesub():
"""验证 DATESUB COPYBOOK 的 REPLACING 展开。"""
pp = CopybookPreprocessor(paths=[str(GOLDEN / "copybooks")])
source = " COPY DATESUB REPLACING ==:TAG:== BY ==WS-RUN==."
result = pp.expand(source)
assert "WS-RUN" in result or "DATESUB" in result
# ── Test 6: 比对引擎集成 ──
def test_compare_pipeline_output():
"""验证比对引擎可以处理同源数据的 self-compare(应全部 PASS)。"""
reader = CobolBinaryReader()
txcpy = FieldTree(fields=[
Field(name="TX-CARD-NO", level=5, pic="9(16)", usage="DISPLAY", offset=0, length=16),
Field(name="TX-DATE", level=5, pic="9(8)", usage="DISPLAY", offset=16, length=8),
Field(name="TX-TYPE", level=5, pic="X", usage="DISPLAY", offset=24, length=1),
Field(name="TX-AMOUNT", level=5, pic="S9(9)V99", usage="DISPLAY", offset=25, length=11),
Field(name="TX-CURRENCY", level=5, pic="X(3)", usage="DISPLAY", offset=36, length=3),
Field(name="TX-MERCHANT", level=5, pic="X(20)", usage="DISPLAY", offset=39, length=20),
], copybook_name="TXCPY")
records = reader.read(str(GOLDEN / "data/input/transactions.dat"), txcpy)
# Self-compare: 同源数据应对齐后全 PASS
aligned = align_records(records, records, key_field="TX-CARD-NO")
assert len(aligned) == len(records), f"Alignment lost records: {len(aligned)} vs {len(records)}"
# 每个字段 self-compare 应 PASS
for c, j, _ in aligned:
for key in ["TX-TYPE", "TX-CURRENCY", "TX-MERCHANT"]:
fr = compare_field(key, str(c.get(key, "")), str(j.get(key, "")), "string")
assert fr.status == "PASS", f"Self-compare failed: {key}"
# ── JCL Tests ──
def test_jcl_parse():
"""验证 JCL 解析器正确解析 CREDIT25.jcl"""
import sys
sys.path.insert(0, str(GOLDEN.parent / "v3-gstack-code-gen"))
from jcl.parser import parse_jcl
jcl_path = str(GOLDEN / "jcl" / "CREDIT25.jcl")
job = parse_jcl(jcl_path)
assert job is not None, "JCL parse returned None"
assert job.job_name == "CREDIT25", f"Expected CREDIT25, got {job.job_name}"
assert len(job.steps) == 4, f"Expected 4 steps, got {len(job.steps)}"
# Step 1: SORT
assert job.steps[0].step_name == "STEP1"
assert job.steps[0].program == "SORT"
assert len(job.steps[0].dd_entries) == 3 # SORTIN, SORTOUT, SYSIN
assert any(dd.dd_name.upper() == "SORTIN" for dd in job.steps[0].dd_entries)
assert any(dd.dd_name.upper() == "SORTOUT" for dd in job.steps[0].dd_entries)
# Step 2: CRDVAL with COND
assert job.steps[1].step_name == "STEP2"
assert job.steps[1].program == "CRDVAL"
assert job.steps[1].cond is not None
assert job.steps[1].cond.code == 0
assert job.steps[1].cond.operator == "NE"
assert len(job.steps[1].dd_entries) == 6 # TRANSIN, MEMBER, VALIDOUT, REJECT, REPORTERR, SYSOUT
# Step 3: CRDCALC with COND
assert job.steps[2].step_name == "STEP3"
assert job.steps[2].program == "CRDCALC"
assert job.steps[2].cond.code == 0
# Step 4: CRDRPT with COND
assert job.steps[3].step_name == "STEP4"
assert job.steps[3].program == "CRDRPT"
assert job.steps[3].cond.code == 0
def test_jcl_dd_mapping():
"""验证 JCL DD 语句正确提取"""
import sys
sys.path.insert(0, str(GOLDEN.parent / "v3-gstack-code-gen"))
from jcl.parser import parse_jcl
job = parse_jcl(str(GOLDEN / "jcl" / "CREDIT25.jcl"))
# Check DD names for CRDVAL step
crdval_dds = {dd.dd_name.upper() for dd in job.steps[1].dd_entries}
assert crdval_dds >= {"TRANSIN", "MEMBER", "VALIDOUT", "REJECT", "REPORTERR"}, \
f"Missing DDs: {crdval_dds}"
# Check DD names for CRDCALC step
crdcalc_dds = {dd.dd_name.upper() for dd in job.steps[2].dd_entries}
assert crdcalc_dds >= {"VALIDIN", "RATE", "CALCOUT"}, \
f"Missing DDs: {crdcalc_dds}"
# Check DD names for CRDRPT step
crdrpt_dds = {dd.dd_name.upper() for dd in job.steps[3].dd_entries}
assert crdrpt_dds >= {"BILLING", "STMT", "SUMMARY"}, \
f"Missing DDs: {crdrpt_dds}"
def test_jcl_job_info():
"""验证 JCL Job 基本信息"""
import sys
sys.path.insert(0, str(GOLDEN.parent / "v3-gstack-code-gen"))
from jcl.parser import parse_jcl
job = parse_jcl(str(GOLDEN / "jcl" / "CREDIT25.jcl"))
assert job.job_name == "CREDIT25"
assert len(job.steps) == 4
programs = [s.program for s in job.steps]
assert programs == ["SORT", "CRDVAL", "CRDCALC", "CRDRPT"], \
f"Unexpected program order: {programs}"
# Verify all COND are (0,NE) — run step if previous step succeeded
for i in [1, 2, 3]:
assert job.steps[i].cond is not None, f"Step {i+1} missing COND"
assert job.steps[i].cond.code == 0
assert job.steps[i].cond.operator == "NE"
+4
View File
@@ -0,0 +1,4 @@
01 BILL-RECORD.
05 BR-AMT PIC S9(7)V99 COMP-3.
05 BR-STATUS PIC X.
05 BR-DATE PIC 9(8).
+1
View File
@@ -0,0 +1 @@
<project><modelVersion>4.0.0</modelVersion><groupId>test</groupId><artifactId>test</artifactId><version>1.0</version><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target></properties><dependencies><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.17.0</version></dependency></dependencies></project>
+13
View File
@@ -0,0 +1,13 @@
program: SIMPLE
field_mapping:
- cobol_field: BR-AMT
java_field: billAmount
type: decimal
precision: 2
- cobol_field: BR-STATUS
java_field: statusCode
type: string
- cobol_field: BR-DATE
java_field: billDate
type: date
format: YYYYMMDD
+13
View File
@@ -0,0 +1,13 @@
IDENTIFICATION DIVISION.
PROGRAM-ID. SIMPLE.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 BILL-RECORD.
05 BR-AMT PIC S9(7)V99 COMP-3.
05 BR-STATUS PIC X.
05 BR-DATE PIC 9(8).
PROCEDURE DIVISION.
DISPLAY BR-AMT.
DISPLAY BR-STATUS.
DISPLAY BR-DATE.
STOP RUN.
+54 -45
View File
@@ -1,80 +1,89 @@
"""Web API layer — wraps orchestrator with 202+ polling pattern.""" """Web API layer — wraps orchestrator with 202+ polling pattern."""
import uuid, json, shutil, sys, os import uuid, json, sys, os
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from fastapi import FastAPI, UploadFile, File, Form, HTTPException from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.requests import Request
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
from config import Config from config import Config
from orchestrator import run_pipeline from orchestrator import run_pipeline
app = FastAPI(title="COBOLJava Verify") app = FastAPI(title="COBOL->Java Verify")
app.mount("/static", StaticFiles(directory=str(Path(__file__).parent / "static")), name="static") BASE = Path(__file__).parent
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates")) app.mount("/static", StaticFiles(directory=str(BASE / "static")), name="static")
TASKS_DIR = Path("tasks") TASKS_DIR = Path("tasks"); TASKS_DIR.mkdir(exist_ok=True)
TASKS_DIR.mkdir(exist_ok=True) UPLOAD_DIR = Path("uploads"); UPLOAD_DIR.mkdir(exist_ok=True)
UPLOAD_DIR = Path("uploads") MAX_SIZE = 10 * 1024 * 1024
UPLOAD_DIR.mkdir(exist_ok=True)
MAX_SIZE = 10 * 1024 * 1024 # 10MB
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def index(request: Request): async def index():
return templates.TemplateResponse("upload.html", {"request": request}) return (BASE / "templates" / "upload.html").read_text(encoding="utf-8")
@app.post("/verify") @app.post("/verify")
async def verify( async def verify(
copybook: UploadFile = File(...), copybook: UploadFile = File(...), cobol_src: UploadFile = File(...),
cobol_src: UploadFile = File(...), java_src: UploadFile = File(...), mapping: UploadFile = File(...),
java_src: UploadFile = File(...),
mapping: UploadFile = File(...),
runner: str = Form("native"), runner: str = Form("native"),
): ):
task_id = str(uuid.uuid4())[:8] task_id = str(uuid.uuid4())[:8]
task_dir = UPLOAD_DIR / task_id task_dir = UPLOAD_DIR / task_id; task_dir.mkdir(parents=True, exist_ok=True)
task_dir.mkdir(parents=True, exist_ok=True) for f, name in [(copybook,"copybook.cpy"),(cobol_src,"program.cbl"),
(java_src,"java"),(mapping,"mapping.yaml")]:
for f, name in [(copybook, "copybook.cpy"), (cobol_src, "program.cbl"),
(java_src, "java"), (mapping, "mapping.yaml")]:
content = await f.read() content = await f.read()
if len(content) > MAX_SIZE: if len(content) > MAX_SIZE:
raise HTTPException(413, f"{f.filename} exceeds 10MB limit") raise HTTPException(413, f"{f.filename} exceeds 10MB")
dest = task_dir / name (task_dir / name).write_bytes(content)
dest.write_bytes(content) (TASKS_DIR / f"{task_id}.json").write_text(json.dumps({
"id":task_id,"status":"queued","copybook":str(task_dir/"copybook.cpy"),
task_file = TASKS_DIR / f"{task_id}.json" "cobol_src":str(task_dir/"program.cbl"),"java_src":str(task_dir/"java"),
task_file.write_text(json.dumps({ "mapping":str(task_dir/"mapping.yaml"),"runner":runner,
"id": task_id, "status": "queued", "created":datetime.now().isoformat()}))
"copybook": str(task_dir / "copybook.cpy"), return JSONResponse({"task_id":task_id,"status":"queued"}, status_code=202)
"cobol_src": str(task_dir / "program.cbl"),
"java_src": str(task_dir / "java"),
"mapping": str(task_dir / "mapping.yaml"),
"runner": runner, "created": datetime.now().isoformat()
}))
return JSONResponse({"task_id": task_id, "status": "queued"}, status_code=202)
@app.get("/status/{task_id}") @app.get("/status/{task_id}")
async def status(task_id: str): async def status(task_id: str):
tf = TASKS_DIR / f"{task_id}.json" tf = TASKS_DIR / f"{task_id}.json"
if not tf.exists(): if not tf.exists(): raise HTTPException(404, "task not found")
raise HTTPException(404, "task not found")
data = json.loads(tf.read_text()) data = json.loads(tf.read_text())
return JSONResponse({"task_id": task_id, "status": data.get("status", "unknown"), return JSONResponse({"task_id":task_id,"status":data.get("status","unknown"),
"result": data.get("result")}) "result":data.get("result"),"fields":data.get("fields",[])})
@app.get("/fields/{task_id}")
async def fields(task_id: str):
tf = TASKS_DIR / f"{task_id}.json"
if not tf.exists(): raise HTTPException(404, "task not found")
data = json.loads(tf.read_text())
return JSONResponse({"task_id":task_id,"fields":data.get("fields",[]),
"debug":data.get("debug",{}),
"build_log":data.get("build_log","")})
@app.get("/result/{task_id}", response_class=HTMLResponse) @app.get("/result/{task_id}", response_class=HTMLResponse)
async def result(request: Request, task_id: str): async def result(task_id: str):
tf = TASKS_DIR / f"{task_id}.json" tf = TASKS_DIR / f"{task_id}.json"
if not tf.exists(): if not tf.exists(): raise HTTPException(404, "task not found")
raise HTTPException(404, "task not found")
data = json.loads(tf.read_text()) data = json.loads(tf.read_text())
return templates.TemplateResponse("result.html", {"request": request, "task": data}) html = (BASE / "templates" / "result.html").read_text(encoding="utf-8")
html = html.replace("{{ task.id }}", data.get("id", task_id))
if data.get("status") == "done" and data.get("result"):
r = data["result"]
html = html.replace("{{ task.status }}", "done")
html = html.replace("{{ task.result.status }}", r.get("status",""))
html = html.replace("{{ task.result.program }}", r.get("program",""))
html = html.replace("{{ task.result.matched }}", str(r.get("matched",0)))
html = html.replace("{{ task.result.mismatched }}", str(r.get("mismatched",0)))
html = html.replace("{{ task.result.runner }}", r.get("runner",""))
html = html.replace("{{ task.result.duration }}", str(r.get("duration",0)))
elif data.get("status") == "error":
html = html.replace("{{ task.status }}", "error")
html = html.replace("{{ task.result.status }}", data.get("result",""))
else:
html = html.replace("{{ task.status }}", data.get("status","queued"))
return HTMLResponse(html)
+21 -10
View File
@@ -1,25 +1,36 @@
document.getElementById("verify-form").addEventListener("submit", async (e) => { document.getElementById("verify-form").addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
const btn = e.target.querySelector("button"); const btn = e.target.querySelector("button[type=submit]");
btn.disabled = true; btn.disabled = true;
btn.textContent = "Submitting..."; btn.textContent = "$ uploading...";
document.getElementById("status").textContent = ""; document.getElementById("status-area").innerHTML = "";
document.getElementById("result").innerHTML = "";
const fd = new FormData(e.target); const fd = new FormData(e.target);
try { try {
const r = await fetch("/verify", { method: "POST", body: fd }); const r = await fetch("/verify", { method: "POST", body: fd });
const d = await r.json(); const d = await r.json();
if (r.ok) { if (r.ok) {
document.getElementById("status").textContent = "Task " + d.task_id + " queued"; document.getElementById("status-area").innerHTML = `
document.getElementById("result").innerHTML = <div class="status-card pending">
'<p><a href="/result/' + d.task_id + '">View result →</a></p>'; <div class="title">&#9679; Queued</div>
Task <code>${d.task_id}</code> submitted. Worker processing.
<div class="matrix"><dt>Runner</dt><dd>${fd.get("runner")}</dd></div>
<a class="result-link" href="/result/${d.task_id}">Open result page &rarr;</a>
</div>`;
} else { } else {
document.getElementById("status").textContent = "Error: " + (d.detail || "unknown"); document.getElementById("status-area").innerHTML = `
<div class="status-card error">
<div class="title">&#10007; Error</div>
${d.detail || "Upload failed"}
</div>`;
} }
} catch (err) { } catch (err) {
document.getElementById("status").textContent = "Upload failed: " + err.message; document.getElementById("status-area").innerHTML = `
<div class="status-card error">
<div class="title">&#10007; Network Error</div>
${err.message}
</div>`;
} }
btn.disabled = false; btn.disabled = false;
btn.textContent = "Verify"; btn.textContent = "$ verify";
}); });
+70 -11
View File
@@ -1,11 +1,70 @@
body { font-family: monospace; max-width: 900px; margin: 2rem auto; padding: 0 1rem; color: #333; } :root {
h1 { font-size: 1.2rem; } --bg: #0a0e14;
label { display: block; margin: .75rem 0; } --panel: #12171f;
input, select { display: block; margin-top: .25rem; } --border: #1f2937;
button { margin-top: 1rem; padding: .5rem 1.5rem; cursor: pointer; font-size: 1rem; } --text: #b2becd;
#status { margin-top: 1rem; font-weight: bold; } --dim: #5c6e80;
#result { margin-top: 1rem; } --accent: #39bae6;
pre { background: #f0f0f0; padding: 1rem; border-radius: 4px; } --green: #7fd962;
.pass { border-left: 4px solid #4caf50; } --red: #f26d78;
.fail { border-left: 4px solid #f44336; } --yellow: #ffad66;
.container a { display: inline-block; margin-top: 1rem; } --font: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font); background: var(--bg); color: var(--text); line-height: 1.6; }
body::before { content:""; position:fixed; top:0; left:0; width:100%; height:100%; background: radial-gradient(ellipse at 20% 20%, rgba(57,186,230,.03) 0%, transparent 50%), radial-gradient(ellipse at 80% 80%, rgba(127,217,98,.02) 0%, transparent 50%); pointer-events:none; z-index:0; }
.container { max-width: 680px; margin: 0 auto; padding: 3rem 1.5rem; position:relative; z-index:1; }
/* Header */
header { margin-bottom: 2.5rem; border-bottom: 1px solid var(--border); padding-bottom: 1.5rem; }
.badge { display:inline-block; padding:.15rem .6rem; background:rgba(57,186,230,.12); color:var(--accent); border-radius:3px; font-size:.7rem; text-transform:uppercase; letter-spacing:.08em; margin-bottom:.75rem; }
h1 { font-size:1.3rem; font-weight:400; color:#e6edf3; line-height:1.3; }
h1 span { color: var(--accent); }
.tagline { font-size:.8rem; color:var(--dim); margin-top:.4rem; }
/* Sections */
section { background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:1.5rem; margin-bottom:1rem; }
section h2 { font-size:.8rem; color:var(--dim); text-transform:uppercase; letter-spacing:.06em; margin-bottom:1rem; font-weight:400; }
/* Form */
.form-grid { display:grid; grid-template-columns:1fr 1fr; gap:.75rem; }
.form-grid .full { grid-column:1/-1; }
label { font-size:.75rem; color:var(--dim); }
label strong { display:block; color:var(--text); font-weight:400; margin-bottom:.25rem; font-size:.8rem; }
input[type="file"], select { width:100%; padding:.55rem .7rem; background:var(--bg); border:1px solid var(--border); border-radius:6px; color:var(--text); font-family:inherit; font-size:.8rem; margin-top:.15rem; transition:border-color .2s; }
input[type="file"]:hover, select:hover { border-color: var(--accent); }
input[type="file"]::file-selector-button { background:var(--border); color:var(--text); border:none; padding:.35rem .8rem; border-radius:4px; margin-right:.5rem; cursor:pointer; font-family:inherit; font-size:.75rem; }
select { cursor:pointer; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%235c6e80'%3E%3Cpath d='M6 8L2 4h8z'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:right .5rem center; appearance:none; padding-right:2rem; }
/* Buttons */
.actions { display:flex; gap:.5rem; margin-top:1.25rem; }
.btn { padding:.6rem 1.25rem; border-radius:6px; font-family:inherit; font-size:.8rem; cursor:pointer; border:none; transition: all .2s; }
.btn-primary { background:var(--accent); color:var(--bg); }
.btn-primary:hover { background:#4fc8f0; }
.btn-primary:disabled { background:var(--border); color:var(--dim); cursor:not-allowed; }
.btn-secondary { background:transparent; color:var(--dim); border:1px solid var(--border); }
.btn-secondary:hover { color:var(--text); border-color:var(--dim); }
/* Status & Results */
#status-area { margin-top:1rem; }
.status-card { background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:1rem 1.5rem; font-size:.8rem; }
.status-card.success { border-left:3px solid var(--green); }
.status-card.pending { border-left:3px solid var(--yellow); }
.status-card.error { border-left:3px solid var(--red); }
.status-card .title { font-weight:600; margin-bottom:.25rem; }
.status-card.success .title { color:var(--green); }
.status-card.pending .title { color:var(--yellow); }
.status-card.error .title { color:var(--red); }
.matrix { display:grid; grid-template-columns:auto 1fr; gap:.3rem 1.5rem; margin:.75rem 0; font-size:.8rem; }
.matrix dt { color:var(--dim); }
.matrix dd { color:var(--text); }
.divider { border-top:1px solid var(--border); margin:1rem 0; }
.result-link { display:inline-block; margin-top:.5rem; color:var(--accent); text-decoration:none; font-size:.8rem; }
.result-link:hover { text-decoration:underline; }
/* Footer */
footer { margin-top:3rem; padding-top:1rem; border-top:1px solid var(--border); font-size:.7rem; color:var(--dim); display:flex; justify-content:space-between; }
footer a { color:var(--dim); text-decoration:none; }
footer a:hover { color:var(--text); }
/* Field table */
table { width:100%; border-collapse:collapse; font-size:.8rem; }
th { text-align:left; color:var(--dim); font-weight:400; padding:.5rem .75rem; border-bottom:1px solid var(--border); }
td { padding:.45rem .75rem; border-bottom:1px solid rgba(31,41,55,.5); }
tr.pass td:first-child { border-left:3px solid var(--green); }
tr.tolerated td:first-child { border-left:3px solid var(--yellow); }
tr.fail td:first-child { border-left:3px solid var(--red); }
pre { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:1rem; font-family:inherit; font-size:.8rem; overflow-x:auto; margin-top:.5rem; }
+89 -17
View File
@@ -1,27 +1,99 @@
<!DOCTYPE html> <!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Result - {{ task.id }}</title> <html lang="en">
<link rel="stylesheet" href="/static/style.css"></head><body> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Result — {{ task.id }}</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container"> <div class="container">
<h1>Verification Result</h1>
{% if task.status == "done" and task.result %} <header>
<pre class="pass">Status: {{ task.result.status }} <div class="badge">Verification Result</div>
Program: {{ task.result.program }} <h1>{{ task.id }}</h1>
Matched: {{ task.result.matched }} | Mismatched: {{ task.result.mismatched }} <div class="tagline">Status: <span class="poll-status">loading...</span></div>
Runner: {{ task.result.runner }} | Duration: {{ task.result.duration }}s</pre> </header>
{% elif task.status == "error" %}
<pre class="fail">{{ task.result }}</pre> <section>
{% else %} <h2>Summary</h2>
<div id="poll-status">Status: {{ task.status }} — polling...</div> <div class="matrix">
<dt>Status</dt><dd>{{ task.result.status }}</dd>
<dt>Program</dt><dd>{{ task.result.program }}</dd>
<dt>Matched</dt><dd>{{ task.result.matched }}</dd>
<dt>Mismatched</dt><dd>{{ task.result.mismatched }}</dd>
<dt>Runner</dt><dd>{{ task.result.runner }}</dd>
<dt>Duration</dt><dd>{{ task.result.duration }}s</dd>
</div>
</section>
<section>
<h2>Field Results</h2>
<div id="fields-table"></div>
</section>
<section>
<h2>Pipeline Details</h2>
<div id="debug-section"></div>
</section>
<div class="divider"></div>
<a href="/" class="btn btn-secondary">← New Verification</a>
<footer>
<span>verify-cli v0.2.0</span>
<span><a href="#">Docs</a> &middot; <a href="#">Architecture</a></span>
</footer>
</div>
<script> <script>
const id = "{{ task.id }}"; const id = "{{ task.id }}";
setInterval(async () => { setInterval(async () => {
const r = await fetch("/status/" + id); const r = await fetch("/status/" + id);
const d = await r.json(); const d = await r.json();
document.getElementById("poll-status").textContent = "Status: " + d.status; document.querySelector(".poll-status").textContent = d.status;
if (d.status === "done" || d.status === "error") location.reload(); if (d.status === "done" || d.status === "error") location.reload();
}, 3000); }, 3000);
// Load per-field results + debug
(async () => {
const r = await fetch("/fields/" + id);
const d = await r.json();
if (d.fields && d.fields.length) {
const rows = d.fields.map(f => {
const cls = f.status === "PASS" ? "pass" : f.status === "TOLERATED" ? "tolerated" : "fail";
return `<tr class="${cls}"><td>${f.name}</td><td>${f.status}</td><td>${f.cobol||""}</td><td>${f.java||""}</td><td>${f.suggestion||""}</td></tr>`;
}).join("");
document.getElementById("fields-table").innerHTML =
`<table><tr><th>Field</th><th>Status</th><th>COBOL</th><th>Java</th><th>Suggestion</th></tr>${rows}</table>`;
}
// Render debug info
const dbg = d.debug || {};
let html = "";
if (dbg.field_tree) {
const treeRows = dbg.field_tree.map(f =>
`<tr><td>L${f.level}</td><td>${f.name}</td><td>${f.pic}</td><td>${f.usage}</td><td>${f.offset}</td><td>${f.length}</td></tr>`).join("");
html += `<h3 style="color:var(--dim);font-size:.75rem;letter-spacing:.05em;margin:1rem 0 .5rem">COPYBOOK FieldTree</h3>
<table><tr><th>Lv</th><th>Name</th><th>PIC</th><th>Usage</th><th>Off</th><th>Len</th></tr>${treeRows}</table>`;
}
if (dbg.test_cases) {
const tcList = dbg.test_cases.map(tc =>
`<tr><td>${tc.id}</td><td>${JSON.stringify(tc.fields)}</td><td>${(tc.targets||[]).join(", ")}</td></tr>`).join("");
html += `<h3 style="color:var(--dim);font-size:.75rem;letter-spacing:.05em;margin:1rem 0 .5rem">Test Data (${dbg.test_cases.length} cases)</h3>
<table><tr><th>ID</th><th>Fields</th><th>Coverage</th></tr>${tcList}</table>`;
}
if (dbg.spark_config) {
html += `<h3 style="color:var(--dim);font-size:.75rem;letter-spacing:.05em;margin:1rem 0 .5rem">Spark Config</h3>
<pre>${dbg.spark_config.records} records via key_varied replication</pre>`;
}
if (dbg.cobol_build) {
html += `<h3 style="color:var(--dim);font-size:.75rem;letter-spacing:.05em;margin:1rem 0 .5rem">COBOL Compile${dbg.cobol_build.ok?"":" (FAILED)"}</h3>
<pre>${dbg.cobol_build.log||"(no output)"}</pre>`;
}
if (dbg.java_build) {
html += `<h3 style="color:var(--dim);font-size:.75rem;letter-spacing:.05em;margin:1rem 0 .5rem">Java Build${dbg.java_build.ok?"":" (FAILED)"}</h3>
<pre>${dbg.java_build.log||"(no output)"}</pre>`;
}
if (html) document.getElementById("debug-section").innerHTML = html;
})();
</script> </script>
{% endif %} </body>
<a href="/">← New verification</a> </html>
</div>
</body></html>
+39 -14
View File
@@ -1,18 +1,43 @@
<!DOCTYPE html> <!DOCTYPE html>
<html><head><meta charset="utf-8"><title>COBOL→Java Verify</title> <html lang="en">
<link rel="stylesheet" href="/static/style.css"></head><body> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>COBOL → Java Migration Verification</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container"> <div class="container">
<h1>COBOL→Java Migration Verification</h1>
<form id="verify-form" enctype="multipart/form-data"> <header>
<label>COPYBOOK:<input type="file" name="copybook" accept=".cpy,.cbl,.copy" required></label> <div class="badge">Developer Tool</div>
<label>COBOL source:<input type="file" name="cobol_src" accept=".cbl" required></label> <h1><span>verify</span> — COBOL to Java/Spark Migration</h1>
<label>Java source (dir with pom.xml):<input type="file" name="java_src" webkitdirectory required></label> <div class="tagline">Automated field-level verification for COBOL→Java migration pipelines</div>
<label>Mapping YAML:<input type="file" name="mapping" accept=".yaml,.yml" required></label> </header>
<label>Runner:<select name="runner"><option value="native">Native Java</option><option value="spark">Spark Java</option></select></label>
<button type="submit">Verify</button> <section>
</form> <h2>Upload Sources</h2>
<div id="status"></div> <form id="verify-form" enctype="multipart/form-data">
<div id="result"></div> <div class="form-grid">
<label><strong>COPYBOOK</strong>.cpy / .cbl / .copy<input type="file" name="copybook" accept=".cpy,.cbl,.copy" required></label>
<label><strong>COBOL Source</strong>.cbl<input type="file" name="cobol_src" accept=".cbl" required></label>
<label><strong>Mapping</strong>.yaml / .yml<input type="file" name="mapping" accept=".yaml,.yml" required></label>
<label><strong>Runner</strong><select name="runner"><option value="native">Native Java</option><option value="spark">Spark Java</option></select></label>
<label class="full"><strong>Java Source</strong>directory with pom.xml<input type="file" name="java_src" webkitdirectory required></label>
</div>
<div class="actions">
<button type="submit" class="btn btn-primary">$ verify</button>
<button type="reset" class="btn btn-secondary">Clear</button>
</div>
</form>
</section>
<div id="status-area"></div>
<footer>
<span>verify-cli v0.2.0</span>
<span><a href="#">Docs</a> &middot; <a href="#">Architecture</a></span>
</footer>
</div> </div>
<script src="/static/script.js"></script> <script src="/static/script.js"></script>
</body></html> </body>
</html>
+8
View File
@@ -32,12 +32,20 @@ def main():
vr = run_pipeline(cfg, data["copybook"], data["cobol_src"], vr = run_pipeline(cfg, data["copybook"], data["cobol_src"],
data["java_src"], data["mapping"]) data["java_src"], data["mapping"])
fields = [{"name":fr.field_name,"status":fr.status,
"cobol":fr.cobol_value,"java":fr.java_value,
"suggestion":fr.suggestion} for fr in vr.field_results]
data["status"] = "done" data["status"] = "done"
data["fields"] = fields
data["debug"] = vr.debug
data["result"] = { data["result"] = {
"program": vr.program, "status": vr.status, "program": vr.program, "status": vr.status,
"matched": vr.fields_matched, "mismatched": vr.fields_mismatched, "matched": vr.fields_matched, "mismatched": vr.fields_mismatched,
"duration": vr.duration_s, "runner": vr.runner, "duration": vr.duration_s, "runner": vr.runner,
} }
if vr.report_path and "BLOCKED" in vr.status:
data["build_log"] = vr.report_path
tf.write_text(json.dumps(data)) tf.write_text(json.dumps(data))
except Exception as e: except Exception as e: