File size: 16,481 Bytes
ebed3db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
"""
seb/adapters/rpg_ingest.py
Cherry-picked from RBG-ibm-meta-corpus/ingest/rpg_matrix.py
Extended: maps RPG data-flow IR to SEB event schema.

Pipeline:
  fixed-format RPG source (80-col)
    β†’ parse rows (F/I/C/O spec columns)
    β†’ extract reads/writes/opcodes
    β†’ build data flow graph
    β†’ map operations to SEB event types
    β†’ emit SEB event schema JSON

SEB event type mapping (from seb_types.ads EventTypeRegistry):
  WRITE  β†’ target is an audit/ledger file  β†’ FISCAL_SETTLE  0x0100
  WRITE  β†’ target is any other output file β†’ INFRA_PROVISION 0x0001
  CHAIN  β†’ DB2 lookup (read by key)        β†’ ARCH_DECISION  0x0010  (query)
  EXSR   β†’ subroutine call                 β†’ CONFIG_DEPLOY  0x0002
  SETON  LR                               β†’ end-of-job      β†’ SOVEREIGN_ROOT 0xFFFF (if LR)

Usage:
  python rpg_ingest.py <path-to-fixed-rpg>        # parse + emit SEB schema
  python rpg_ingest.py <path> --seb-events        # show SEB event list only
  python rpg_ingest.py <path> --adapter-stub      # emit RPGLE adapter stub

Dependencies: stdlib only (no pip installs)
"""

from __future__ import annotations

import json
import re
import sys
from collections import Counter
from pathlib import Path

# ── Config (inlined from config/column_map.json) ─────────────────────────────
COLUMN_MAP: dict = {
    "format": "fixed-rpg-80",
    "base_columns": {
        "sequence":     {"start": 1,  "end": 5},
        "spec":         {"start": 6,  "end": 6},
        "comment_tail": {"start": 54, "end": 80},
    },
    "layouts": {
        "token_window": {"start": 7, "end": 60}
    },
    "spec_types": {
        "H": "header", "F": "file",   "E": "extension",
        "L": "line_counter", "I": "input", "C": "calculation",
        "O": "output", "*": "comment",
    },
    "opcode_classes": {
        "ADD": "arithmetic",  "SUB": "arithmetic",  "MULT": "arithmetic",
        "DIV": "arithmetic",  "Z-ADD": "arithmetic", "MOVE": "move",
        "MOVEL": "move",      "CHAIN": "io",         "READ": "io",
        "WRITE": "io",        "READE": "io",         "READP": "io",
        "IFEQ": "condition",  "IFNE": "condition",   "IFLT": "condition",
        "IFGT": "condition",  "IFLE": "condition",   "IFGE": "condition",
        "ELSE": "condition",  "END": "condition",    "BEGSR": "subroutine",
        "ENDSR": "subroutine","EXSR": "subroutine",  "PLIST": "parameter",
        "PARM": "parameter",  "SETON": "indicator",  "SETOF": "indicator",
        "CALL": "call",       "RETURN": "call",
    }
}

# ── SEB event type codes ──────────────────────────────────────────────────────
SEB_INFRA_PROVISION  = 0x0001   # execute capability
SEB_CONFIG_DEPLOY    = 0x0002   # write capability
SEB_ARCH_DECISION    = 0x0010   # verify capability (DB2 query / CHAIN)
SEB_FISCAL_SETTLE    = 0x0100   # execute + weight=MAX (ledger write)
SEB_SOVEREIGN_ROOT   = 0xFFFF   # vacuum_collapse (LR seton = end job)

# Heuristics: file name patterns that indicate fiscal/ledger targets
FISCAL_PATTERNS = re.compile(
    r'(LEDGER|AUDIT|SETTLE|FISCAL|GL_|PAY|JOURNAL|JRN|LEDGE|VAULT)',
    re.IGNORECASE
)

TOKEN_RE = re.compile(r"\S+")


# ── Parser (from rpg_matrix.py, unchanged) ────────────────────────────────────

def pad_line(line: str, width: int = 80) -> str:
    return line.rstrip("\n").rstrip("\r")[:width].ljust(width)

def slice_1(text: str, start: int, end: int) -> str:
    return text[start - 1 : end]

def normalize(s: str) -> str | None:
    v = " ".join(s.strip().split())
    return v or None

def classify_opcode(op: str) -> str:
    return COLUMN_MAP["opcode_classes"].get(op.strip().upper(), "unknown")

def extract_tokens(line: str, start: int = 7, end: int = 53) -> list[str]:
    return TOKEN_RE.findall(slice_1(line, start, end))

def find_opcode_idx(tokens: list[str]) -> int | None:
    for i, t in enumerate(tokens):
        if t.strip().upper() in COLUMN_MAP["opcode_classes"]:
            return i
    return None

def base_row(line: str, lineno: int) -> tuple[dict, str]:
    cols = COLUMN_MAP["base_columns"]
    spec = slice_1(line, cols["spec"]["start"], cols["spec"]["end"]).strip().upper()
    if not spec and slice_1(line, 7, 7) == "*":
        spec = "*"
    row = {
        "line_number": lineno,
        "raw": line,
        "spec_code": spec,
        "spec_kind": COLUMN_MAP["spec_types"].get(spec, "unknown"),
        "fields": {"sequence": slice_1(line, 1, 5).rstrip(),
                   "comment": slice_1(line, cols["comment_tail"]["start"],
                                      cols["comment_tail"]["end"]).rstrip()},
        "classification": {"domain": COLUMN_MAP["spec_types"].get(spec, "unknown"),
                           "is_comment": spec == "*",
                           "is_blank": not line.strip()},
        "reads": [], "writes": [], "warnings": [],
    }
    return row, spec

def decode_file_row(line: str, row: dict) -> dict:
    toks = extract_tokens(line)
    name = toks[0] if toks else ""
    row["fields"].update({"file_name": name, "access": toks[1] if len(toks) > 1 else "",
                          "keywords": toks[2:]})
    if name: row["writes"].append(name)
    else: row["warnings"].append("file row missing file_name")
    return row

def decode_input_row(line: str, row: dict) -> dict:
    toks = extract_tokens(line)
    if not toks:
        row["warnings"].append("input row has no tokens"); return row
    if toks[0].isdigit():
        row["classification"]["domain"] = "input_field"
        fn = toks[2] if len(toks) > 2 else ""
        row["fields"].update({"input_shape": "field", "field_from": toks[0],
                              "field_to": toks[1] if len(toks) > 1 else "",
                              "field_name": fn})
        if fn: row["writes"].append(fn)
    else:
        row["classification"]["domain"] = "input_record"
        row["fields"].update({"input_shape": "record", "record_name": toks[0]})
        row["reads"].append(toks[0])
    return row

def decode_calculation_row(line: str, row: dict) -> dict:
    toks = extract_tokens(line)
    idx = find_opcode_idx(toks)
    comment = slice_1(line, 54, 80).rstrip()
    if idx is None:
        row["fields"].update({"factor1": "", "opcode": "", "factor2": "",
                              "result": "", "comment": comment, "tokens": toks})
        row["classification"]["domain"] = "unknown"
        row["warnings"].append("calculation row missing recognized opcode")
        return row
    op = toks[idx].strip().upper()
    before = toks[:idx]; after = toks[idx + 1:]
    f1 = before[-1] if before else ""
    f2 = after[0] if after else ""
    result = after[1] if len(after) > 1 else ""
    if op in {"WRITE", "SETON", "SETOF", "END", "ELSE", "BEGSR", "ENDSR", "EXSR"}:
        result = ""
    row["fields"].update({"factor1": f1, "opcode": op, "factor2": f2,
                          "result": result, "comment": comment, "tokens": toks})
    row["classification"]["domain"] = classify_opcode(op)
    reads, writes = [], []
    if f1 := normalize(f1): reads.append(f1)
    if f2v := normalize(f2):
        if op not in {"WRITE", "SETON"}: reads.append(f2v)
    if rv := normalize(result):
        if op in {"Z-ADD","ADD","SUB","MULT","DIV","MOVEL","MOVE","PARM"}: writes.append(rv)
    if op == "CHAIN" and f2v: reads.append(f2v)
    if op == "WRITE" and f2v: writes.append(f2v)
    if op == "SETON" and f2v: writes.append(f2v)
    if op in {"BEGSR","EXSR","ENDSR"} and f1: writes.append(f1)
    row["reads"] = list(dict.fromkeys(reads))
    row["writes"] = list(dict.fromkeys(writes))
    return row

def decode_output_row(line: str, row: dict) -> dict:
    toks = extract_tokens(line)
    rec = toks[0] if toks else ""
    row["classification"]["domain"] = "output"
    row["fields"].update({"output_record": rec,
                          "operation": toks[1] if len(toks) > 1 else "",
                          "operands": toks[2:]})
    if rec: row["writes"].append(rec)
    else: row["warnings"].append("output row missing output_record")
    return row

def decode_row(line: str, lineno: int) -> dict:
    row, spec = base_row(line, lineno)
    if row["classification"]["is_blank"] or row["classification"]["is_comment"]:
        return row
    if spec == "F": return decode_file_row(line, row)
    if spec == "I": return decode_input_row(line, row)
    if spec == "C": return decode_calculation_row(line, row)
    if spec == "O": return decode_output_row(line, row)
    return row

def build_matrix(lines: list[str]) -> list[list[int]]:
    return [[ord(ch) for ch in line] for line in lines]

def build_flow(rows: list[dict]) -> dict:
    nodes, edges = [], []
    for row in rows:
        nid = f"line_{row['line_number']}"
        nodes.append({"id": nid, "line_number": row["line_number"],
                      "spec_kind": row["spec_kind"],
                      "opcode": row["fields"].get("opcode", "").strip(),
                      "domain": row["classification"]["domain"],
                      "warnings": row["warnings"]})
        for s in row["reads"]:  edges.append({"type": "reads",  "symbol": s, "target": nid})
        for s in row["writes"]: edges.append({"type": "writes", "symbol": s, "source": nid})
    return {"nodes": nodes, "edges": edges}

def build_summary(rows: list[dict]) -> dict:
    return {"spec_counts": dict(Counter(r["spec_kind"] for r in rows)),
            "domain_counts": dict(Counter(r["classification"]["domain"] for r in rows)),
            "warnings": [{"line_number": r["line_number"], "warnings": r["warnings"]}
                         for r in rows if r["warnings"]]}


# ── SEB event mapping (new β€” not in original rpg_matrix.py) ──────────────────

def row_to_seb_event(row: dict) -> dict | None:
    """Map a single RPG calculation row to a SEB event descriptor."""
    op = row["fields"].get("opcode", "").strip().upper()
    if not op:
        return None

    writes = row.get("writes", [])
    reads  = row.get("reads",  [])
    lineno = row["line_number"]

    # WRITE to fiscal/ledger target β†’ FISCAL_SETTLE
    if op == "WRITE":
        for w in writes:
            if FISCAL_PATTERNS.search(w):
                return {"line": lineno, "opcode": op, "target": w,
                        "seb_event_type": SEB_FISCAL_SETTLE,
                        "seb_event_name": "FISCAL_SETTLE",
                        "required_capability": "execute",
                        "weight": 0xFFFFFFFF,
                        "human_review_required": True}
        for w in writes:
            return {"line": lineno, "opcode": op, "target": w,
                    "seb_event_type": SEB_INFRA_PROVISION,
                    "seb_event_name": "INFRA_PROVISION",
                    "required_capability": "execute",
                    "weight": 1000,
                    "human_review_required": False}

    # CHAIN β†’ DB2 read-by-key β†’ ARCH_DECISION (query event)
    if op == "CHAIN":
        target = reads[-1] if reads else "unknown"
        return {"line": lineno, "opcode": op, "target": target,
                "seb_event_type": SEB_ARCH_DECISION,
                "seb_event_name": "ARCH_DECISION",
                "required_capability": "verify",
                "weight": 100,
                "human_review_required": False}

    # EXSR β†’ subroutine call β†’ CONFIG_DEPLOY
    if op == "EXSR":
        sub = reads[0] if reads else writes[0] if writes else "unknown"
        return {"line": lineno, "opcode": op, "target": sub,
                "seb_event_type": SEB_CONFIG_DEPLOY,
                "seb_event_name": "CONFIG_DEPLOY",
                "required_capability": "write",
                "weight": 500,
                "human_review_required": False}

    # SETON LR β†’ end-of-job β†’ SOVEREIGN_ROOT
    if op == "SETON":
        factor2 = row["fields"].get("factor2", "").strip().upper()
        if "LR" in factor2 or "LR" in writes:
            return {"line": lineno, "opcode": op, "target": "LR",
                    "seb_event_type": SEB_SOVEREIGN_ROOT,
                    "seb_event_name": "SOVEREIGN_ROOT",
                    "required_capability": "vacuum_collapse",
                    "weight": 0xFFFFFFFF,
                    "human_review_required": True}
    return None


def extract_seb_events(rows: list[dict]) -> list[dict]:
    events = []
    for row in rows:
        if row["spec_code"] == "C":
            evt = row_to_seb_event(row)
            if evt:
                events.append(evt)
    return events


def emit_adapter_stub(source_name: str, events: list[dict]) -> str:
    """Emit an RPGLE adapter stub that emits the detected SEB events."""
    lines = [
        f"** SEB adapter stub generated from {source_name}",
        f"** Cherry-picked rpg_ingest.py from RBG-ibm-meta-corpus",
        f"** DO NOT EDIT β€” regenerate with: python rpg_ingest.py {source_name} --adapter-stub",
        "**FREE",
        "ctl-opt dftactgrp(*no) actgrp(*new) bnddir('SEB_BND');",
        "",
        "// SEB kernel entry points",
        "dcl-pr SEB_Append_Event extproc(*dclcase);",
        "  Hdr pointer value options(*nopass);",
        "  Pay pointer value options(*nopass);",
        "  Ftr pointer value options(*nopass);",
        "end-pr;",
        "",
        "dcl-pr SEB_Worm_Flush extproc(*dclcase);",
        "end-pr;",
        "",
    ]

    for evt in events:
        ename = evt["seb_event_name"]
        etype = evt["seb_event_type"]
        cap   = evt["required_capability"]
        hr    = evt["human_review_required"]
        target = evt["target"]
        lines += [
            f"// Line {evt['line']}: {evt['opcode']} {target}",
            f"// SEB event: {ename} (0x{etype:04X}) cap={cap} human_review={hr}",
            f"dcl-proc Emit_{ename}_{evt['line']} export;",
            f"  // TODO: build 68-byte Event_Header with Event_Type = 0x{etype:04X}",
            f"  // TODO: build payload from {target} record",
            f"  // TODO: call SEB_Append_Event(hdr_ptr : pay_ptr : ftr_ptr);",
            f"  // TODO: if human_review_required call SEB_Human_Review_Queue;",
            f"end-proc;",
            "",
        ]

    lines += ["// Flush WORM chain after all events emitted",
              "SEB_Worm_Flush();", "*inlr = *on;"]
    return "\n".join(lines)


# ── Main ──────────────────────────────────────────────────────────────────────

def main(argv: list[str]) -> int:
    if len(argv) < 2:
        print("usage: python rpg_ingest.py <path-to-fixed-rpg> [--seb-events] [--adapter-stub]")
        return 1

    source_path = Path(argv[1]).resolve()
    if not source_path.exists():
        print(f"error: not found: {source_path}"); return 1

    mode_events  = "--seb-events"  in argv
    mode_adapter = "--adapter-stub" in argv

    lines = [pad_line(l) for l in source_path.read_text(encoding="utf-8").splitlines()]
    rows  = [decode_row(l, i + 1) for i, l in enumerate(lines)]
    flow  = build_flow(rows)
    summary = build_summary(rows)
    events  = extract_seb_events(rows)

    if mode_adapter:
        print(emit_adapter_stub(source_path.name, events))
        return 0

    if mode_events:
        print(json.dumps(events, indent=2))
        return 0

    # Default: full IR output
    out = {
        "source": str(source_path),
        "shape": [len(rows), 80],
        "summary": summary,
        "seb_events": events,
        "flow": flow,
        "rows": rows,
        "matrix": build_matrix(lines),
    }
    print(json.dumps(out, indent=2))

    # Summary to stderr
    import sys as _sys
    print(f"\nsource:     {source_path}", file=_sys.stderr)
    print(f"specs:      {summary['spec_counts']}", file=_sys.stderr)
    print(f"seb_events: {len(events)}", file=_sys.stderr)
    for e in events:
        print(f"  line {e['line']:4d} {e['opcode']:8s} β†’ {e['seb_event_name']} (0x{e['seb_event_type']:04X})",
              file=_sys.stderr)
    print(f"warnings:   {len(summary['warnings'])}", file=_sys.stderr)
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv))