Use this skill for Phase 04 of mainframe migration: logic translation and code synthesis. Triggers when the user runs /migrate run 04, mentions COBOL to Java translation, JOBOL anti-pattern avoidance, business rule extraction, COMP-3 converter generation, GO TO elimination, or EBCDIC encoding handlers. Requires Phase 03 complete. This is where the actual code gets rewritten.
Translate COBOL business logic into idiomatic, modern Java (or C#) — not "JOBOL" (Java that thinks it's still COBOL). Strip data access and terminal routing boilerplate, extract pure business logic, then restructure it using proper OO patterns.
Output directory: src/ (generated source tree, ready for compilation)
The most common failure in COBOL migration is producing code like this:
// ❌ JOBOL — Java written like COBOL
public class CUSTPROG {
static String WS_CUSTOMER_ID = "";
static String WS_CUSTOMER_NAME = "";
static int WS_RETURN_CODE = 0;
public static void main(String[] args) {
PROCEDURE_DIVISION();
}
static void PROCEDURE_DIVISION() {
INIT_SECTION();
PROCESS_SECTION();
FINALIZE_SECTION();
}
static void INIT_SECTION() { ... } // ← paragraph mapped 1:1
static void PROCESS_SECTION() { ... }
static void FINALIZE_SECTION() { ... }
}
This has all of COBOL's problems and none of Java's benefits.
Produce this instead:
// ✅ Idiomatic Java
@Service
public class CustomerService {
private final CustomerRepository customerRepository;
public CustomerService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public CustomerResponse processCustomer(CustomerRequest request) {
Customer customer = customerRepository.findById(request.customerId())
.orElseThrow(() -> new CustomerNotFoundException(request.customerId()));
return CustomerResponse.builder()
.customerId(customer.getId())
.name(customer.getName())
.status(customer.isActive() ? "ACTIVE" : "INACTIVE")
.build();
}
}
import json
with open("docs/01_inventory/program_inventory.json") as f:
programs = json.load(f)
with open("docs/01_inventory/comp3_registry.json") as f:
comp3_fields = json.load(f)
with open("docs/03_architecture/microservice_boundaries.json") as f:
services = json.load(f)
with open("docs/02_profiling/migration_priority_matrix.csv") as f:
import pandas as pd
matrix = pd.read_csv(f)
with open("MIGRATION_STATE.json") as f:
state = json.load(f)
target_lang = state["target_language"] # Java | C#
Before translating, clean and annotate each COBOL source file.
def annotate_cobol_paragraphs(source):
"""
Classify COBOL paragraphs by function to guide translation.
Returns dict of paragraph_name → classification.
"""
paragraphs = {}
current_para = None
for line in source.split('\n'):
# Paragraph header (columns 8-72, no leading spaces, ends with .)
if re.match(r'^[A-Z][A-Z0-9-]+-?\.?\s*$', line.strip()):
current_para = line.strip().rstrip('.')
paragraphs[current_para] = classify_paragraph(current_para, source)
return paragraphs
def classify_paragraph(name, source):
"""Classify paragraph role from name and content patterns."""
name_upper = name.upper()
if any(k in name_upper for k in ["INIT", "SETUP", "START", "BEGIN"]):
return "INITIALIZATION"
elif any(k in name_upper for k in ["READ", "FETCH", "GET", "INQ"]):
return "DATA_ACCESS"
elif any(k in name_upper for k in ["WRITE", "UPDATE", "SAVE", "PUT", "INS"]):
return "DATA_MUTATION"
elif any(k in name_upper for k in ["DISPLAY", "SEND", "SCREEN", "MAP"]):
return "UI_PRESENTATION" # ← do NOT translate business logic from here
elif any(k in name_upper for k in ["CALC", "COMP", "PROCESS", "VALIDATE", "CHECK"]):
return "BUSINESS_LOGIC" # ← primary translation target
elif any(k in name_upper for k in ["ERROR", "ABEND", "EXCEPTION", "ERR"]):
return "ERROR_HANDLING"
elif any(k in name_upper for k in ["EXIT", "END", "STOP", "FINAL"]):
return "TERMINATION"
return "UNKNOWN"
GO TO in COBOL is typically used for:
throw new XxxException()break or while conditiondef classify_goto(source):
"""Find and classify all GO TO statements."""
gotos = []
lines = source.split('\n')
for i, line in enumerate(lines):
m = re.search(r'\bGO\s+TO\s+(\w[\w-]*)', line, re.I)
if m:
target = m.group(1)
context = '\n'.join(lines[max(0,i-5):i+5])
if re.search(r'ERROR|ABEND|EXCEPTION', target, re.I):
go_type = "ERROR_BRANCH" # → throw Exception
elif re.search(r'EXIT|END-OF|BOTTOM', target, re.I):
go_type = "LOOP_EXIT" # → break
else:
go_type = "LOGIC_SKIP" # → restructure
gotos.append({
"line": i+1,
"target": target,
"type": go_type,
"context": context
})
return gotos
Strip the infrastructure (data access, screen I/O, error boilerplate) and isolate the pure business logic paragraphs.
INFRASTRUCTURE_PATTERNS = [
r'EXEC\s+SQL', # DB2 SQL — replaced by repository pattern
r'EXEC\s+CICS', # CICS API — replaced by Spring/REST
r'EXEC\s+IMS', # IMS DLI — replaced by repository
r'READ\s+\w+\s+FILE', # VSAM file I/O — replaced by repository
r'WRITE\s+\w+\s+FROM', # VSAM write — replaced by repository
r'SEND\s+MAP', # BMS screen — moved to Angular
r'RECEIVE\s+MAP', # BMS screen — moved to Angular
r'DISPLAY\s+WS-', # console output — moved to logging
]
def extract_business_rules(source, paragraph_classifications):
"""Extract paragraphs classified as BUSINESS_LOGIC."""
business_paragraphs = {
name: para for name, para in paragraph_classifications.items()
if para == "BUSINESS_LOGIC"
}
rules = []
for para_name in business_paragraphs:
# Extract the paragraph body
pattern = rf'{para_name}\.(.+?)(?=\n[A-Z][A-Z0-9-]+\.|\Z)'
m = re.search(pattern, source, re.S | re.I)
if m:
body = m.group(1)
# Check if it's genuinely business logic (no infrastructure)
has_infrastructure = any(
re.search(p, body, re.I) for p in INFRASTRUCTURE_PATTERNS
)
rules.append({
"paragraph": para_name,
"body": body.strip(),
"is_pure_logic": not has_infrastructure,
"line_count": body.count('\n')
})
return rules
Every COMP-3 field from docs/01_inventory/comp3_registry.json needs
a Java converter class. Generate these first — all translated programs
depend on them.
def generate_java_comp3_utils(comp3_fields):
"""Generate MainframeDecimalUtils.java for COMP-3 field handling."""
class_body = '''package com.migration.util;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
/**
* Utility class for converting mainframe COMP-3 (packed decimal) values.
* COMP-3 packs two decimal digits per byte, with the sign in the low nibble
* of the last byte (C=positive, D=negative, F=unsigned).
* Generated by mainframe-migration Phase 04.
*/
public class MainframeDecimalUtils {
// Mainframe uses HALF_EVEN (banker\'s rounding) by default
public static final RoundingMode MAINFRAME_ROUNDING = RoundingMode.HALF_EVEN;
/**
* Decode a COMP-3 packed decimal byte array to BigDecimal.
* @param packed Raw bytes from mainframe record
* @param scale Number of decimal places (from PIC clause V digits)
*/
public static BigDecimal fromComp3(byte[] packed, int scale) {
if (packed == null || packed.length == 0) return BigDecimal.ZERO;
StringBuilder digits = new StringBuilder();
boolean negative = false;
for (int i = 0; i < packed.length; i++) {
int b = packed[i] & 0xFF;
if (i < packed.length - 1) {
// Two digits per byte
digits.append((b >> 4) & 0xF);
digits.append(b & 0xF);
} else {
// Last byte: high nibble = last digit, low nibble = sign
digits.append((b >> 4) & 0xF);
int sign = b & 0x0F;
negative = (sign == 0xD); // D = negative, C/F = positive
}
}
BigDecimal result = new BigDecimal(digits.toString())
.scaleByPowerOfTen(-scale);
return negative ? result.negate() : result;
}
/**
* Encode a BigDecimal to COMP-3 packed decimal bytes.
*/
public static byte[] toComp3(BigDecimal value, int totalDigits, int scale) {
value = value.setScale(scale, MAINFRAME_ROUNDING);
String digits = value.abs().unscaledValue().toString();
// Pad to totalDigits
while (digits.length() < totalDigits) digits = "0" + digits;
int byteCount = (totalDigits + 1) / 2;
byte[] packed = new byte[byteCount];
// ... packing logic
return packed;
}
'''
# Add field-specific constants
for field in comp3_fields:
name = field["name"].replace("-", "_").upper()
pic = field["pic"]
# Count integer and decimal digits from PIC
int_digits = len(re.findall(r'9', pic.split('V')[0]))
dec_digits = len(re.findall(r'9', pic.split('V')[1])) if 'V' in pic else 0
class_body += f'''
// Field: {field['name']} — {pic} (COMP-3, {field['bytes']} bytes)
public static final int {name}_TOTAL_DIGITS = {int_digits + dec_digits};
public static final int {name}_SCALE = {dec_digits};
public static final int {name}_BYTES = {field['bytes']};
'''
class_body += "\n}\n"
return class_body
Write to src/main/java/com/migration/util/MainframeDecimalUtils.java.
Translate each program systematically:
COBOL_TO_JAVA_PATTERNS = [
# Arithmetic
(r'ADD\s+(\w+)\s+TO\s+(\w+)', r'\2 += \1;'),
(r'SUBTRACT\s+(\w+)\s+FROM\s+(\w+)', r'\2 -= \1;'),
(r'MULTIPLY\s+(\w+)\s+BY\s+(\w+)\s+GIVING\s+(\w+)', r'\3 = \1.multiply(\2);'),
(r'DIVIDE\s+(\w+)\s+INTO\s+(\w+)\s+GIVING\s+(\w+)', r'\3 = \2.divide(\1, MAINFRAME_ROUNDING);'),
# String operations
(r'MOVE\s+SPACES\s+TO\s+(\w+)', r'\1 = "";'),
(r'MOVE\s+ZEROS\s+TO\s+(\w+)', r'\1 = BigDecimal.ZERO;'),
(r'MOVE\s+(\w+)\s+TO\s+(\w+)', r'\2 = \1;'),
# Conditions
(r'IF\s+(\w+)\s+=\s+SPACES', r'if (\1.isBlank()) {'),
(r'IF\s+(\w+)\s+=\s+ZEROS', r'if (\1.compareTo(BigDecimal.ZERO) == 0) {'),
(r'END-IF', r'}'),
# Loops
(r'PERFORM\s+(\w+)\s+VARYING\s+(\w+)\s+FROM\s+(\d+)\s+BY\s+(\d+)\s+UNTIL\s+(\w+)\s*>\s*(\w+)',
r'for (int \2 = \3; \2 <= \6; \2 += \4) {'),
# Error handling
(r'GO\s+TO\s+(\w*ERROR\w*)', r'throw new MainframeRuntimeException("Error branch: \1");'),
(r'MOVE\s+(\d+)\s+TO\s+RETURN-CODE', r'throw new MainframeReturnCodeException(\1);'),
]
def translate_cobol_to_java(cobol_body, field_types):
"""Apply translation patterns to a COBOL paragraph body."""
java_lines = []
for cobol_line in cobol_body.split('\n'):
cobol_line = cobol_line.strip()
if not cobol_line or cobol_line.startswith('*'):
continue # skip blank lines and comments
java_line = cobol_line
for pattern, replacement in COBOL_TO_JAVA_PATTERNS:
java_line = re.sub(pattern, replacement, java_line, flags=re.I)
java_lines.append(f" {java_line}")
return '\n'.join(java_lines)
def generate_java_service(program, business_rules, service_assignment, target_lang):
"""Generate a complete Java Spring service class from extracted business rules."""
class_name = to_pascal_case(program["program_id"])
package = f"com.migration.{service_assignment['service_id'].replace('-', '.')}"
header = f'''package {package};
import com.migration.util.MainframeDecimalUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Optional;
/**
* Migrated from COBOL program: {program["program_id"]}
* Source LOC: {program["loc"]}
* Service: {service_assignment["service_id"]}
* Generated by mainframe-migration Phase 04 — review before production use.
*/
@Service
public class {class_name}Service {{
'''
methods = ""
for rule in business_rules:
if rule["is_pure_logic"]:
method_name = to_camel_case(rule["paragraph"])
translated = translate_cobol_to_java(rule["body"], {})
methods += f'''
/**
* Migrated from COBOL paragraph: {rule["paragraph"]}
* TODO: Review translation — verify against original COBOL output
*/
@Transactional
public void {method_name}() {{
{translated}
}}
'''
footer = "\n}\n"
return header + methods + footer
def to_pascal_case(name):
return ''.join(w.capitalize() for w in name.replace('-', '_').split('_'))
def to_camel_case(name):
parts = name.replace('-', '_').split('_')
return parts[0].lower() + ''.join(w.capitalize() for w in parts[1:])
Write to src/main/java/com/migration/<service_id>/<ProgramName>Service.java.
def generate_ebcdic_utils():
return '''package com.migration.util;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* Handles EBCDIC (Code Page 037) to UTF-8 string conversion.
* Mainframe character encoding is EBCDIC — all string fields
* from mainframe records must pass through these methods.
* Generated by mainframe-migration Phase 04.
*/
public class EbcdicUtils {
private static final Charset EBCDIC = Charset.forName("Cp037");
public static String fromEbcdic(byte[] bytes) {
return new String(bytes, EBCDIC).trim();
}
public static byte[] toEbcdic(String value, int fieldLength) {
byte[] ebcdic = value.getBytes(EBCDIC);
// Pad or truncate to mainframe field length (space = 0x40 in EBCDIC)
byte[] result = new byte[fieldLength];
java.util.Arrays.fill(result, (byte) 0x40); // EBCDIC space
System.arraycopy(ebcdic, 0, result, 0, Math.min(ebcdic.length, fieldLength));
return result;
}
/** Convert mainframe date format YYYYDDD (Julian) to ISO YYYY-MM-DD */
public static String julianToIso(String julian) {
int year = Integer.parseInt(julian.substring(0, 4));
int day = Integer.parseInt(julian.substring(4));
return java.time.LocalDate.ofYearDay(year, day).toString();
}
/** Convert mainframe packed date YYYYMMDD to ISO */
public static String mainframeDateToIso(String packed) {
return packed.substring(0, 4) + "-" + packed.substring(4, 6) + "-" + packed.substring(6);
}
}
'''
Write to src/main/java/com/migration/util/EbcdicUtils.java.
After translating all programs, produce a quality report:
def generate_translation_report(translated_programs):
report = {
"total_programs": len(translated_programs),
"auto_translated": 0,
"needs_review": [],
"failed": [],
"goto_eliminated": 0,
"comp3_fields_handled": 0
}
for prog in translated_programs:
if prog["status"] == "auto":
report["auto_translated"] += 1
elif prog["status"] == "needs_review":
report["needs_review"].append({
"program": prog["program_id"],
"reason": prog["review_reason"]
})
elif prog["status"] == "failed":
report["failed"].append(prog["program_id"])
return report
Write docs/04_translation/translation_report.json.
When a translation is uncertain, insert a TODO comment rather than