Most COBOL modernization programs are sold as a translation problem. Feed the old code to a capable model, get modern code back, repeat until the mainframe is gone. That framing is a large part of why 70 to 80 percent of these projects miss their objectives (industry meta-analysis, 2025). The code that breaks the migration is almost never the code the translator can see. It is a data type decided three copybooks away, an alias that reuses the same bytes as text, a flag set by a different program and sequenced by a batch job that runs at 2 a.m. A single-file context window, however large the model behind it, never contained those facts to begin with. This is a retrieval problem, not a reasoning problem, and a better model does not touch it.
We built CodeGraph to invert the order of operations: build the dependency map first, then let anything downstream act on it. It parses a COBOL estate into a typed knowledge graph and resolves the full transitive dependency closure of a change, carrying file:line provenance on every edge. You can run the demo at https://veriprajna.com/demos/legacy-cobol-modernization. The estate inside it is synthetic and authored on purpose, and that is the point. Because we wrote the codebase, its true dependency set is known, so every number below is a reproducible measurement against a labeled ground truth rather than a marketing claim.
Why these projects fail at understanding, not translation
There are still roughly 220 billion lines of COBOL running in production, much of it inside core banking ledgers and payment rails (industry meta-analysis, 2025). The programs that need to move first are exactly the ones whose behavior is spread across the estate: a copybook here, a DB2 table there, a JCL schedule that decides what runs before what. A commodity translator reads the one file it can see and treats code as text. The fact that actually governs correctness lives in the topology between files, and topology is invisible in a single-file window.
That is the whole reframe. Modernization is not primarily a code-generation task. It is a task of retrieving the exact transitive slice a change touches and proving you retrieved all of it. Translation is a downstream use case that depends on getting the map right first. The map is the product.
The one dependency that corrupts a wire transfer
Here is the shape of the failure, worked end to end on the demo's wire-transfer subsystem. The program WIRETXN runs a COMPUTE on a variable called TRN-LIMIT. In the single file, that line looks trivial. Against the known ground-truth dependency set for that change, the graph recovers 9 out of 9 facts. A simulated single-file context window recovers 3 out of 9.
The three the single file gets are the easy ones: WIRETXN uses TRN-LIMIT in a COMPUTE (WIRETXN.cbl:33), it imports copybook CBACCT by name (WIRETXN.cbl:13), and it issues an UPDATE on DB2 table ACCOUNTS (WIRETXN.cbl:37). The six it cannot see are the ones that decide correctness. TRN-LIMIT is declared PIC S9(9)V99 COMP-3, a packed decimal that must map to BigDecimal and not long (CBACCT.cpy:11). TRN-LIMIT-ALPHA REDEFINES TRN-LIMIT, reusing the same six bytes as text (CBACCT.cpy:12). A field named LIMIT-TYPE-FLAG decides which interpretation is live (CBACCT.cpy:13). Two different programs, LIMITSET and BATCHUPD, write that flag, and a JCL job named NIGHTLY at 02:00 is a scheduled predecessor of the wire run, so the flag is set before the transfer executes. That last edge exists only in the JCL, nowhere in any COBOL source.
The Impact panel for the TRN-LIMIT change in the running demo. Graph retrieval recovers 9 of 9 dependency facts; a naive single-file context recovers 3 of 9. F4 to F6, marked critical, are the ones hidden in the single file: the COMP-3 packed-decimal type, the REDEFINES alias, and the LIMIT-TYPE-FLAG that decides which interpretation is live, each with its CBACCT.cpy line number.
Toggle the "Naive AI context view" and the graph dims to the one file plus a few lines around the change. Six of the nine facts grey out on screen, a red banner spells out the consequence, and the point becomes concrete: handed only the three visible facts, a model emits long TRN_LIMIT, truncates a packed-decimal amount, and writes corrupted bytes into ACCOUNTS. The Java compiles. It passes unit tests. It fails in UAT on the first live transfer. That is the entire category's failure mode in one screen.
The naive single-file view. The banner states that only 3 of 9 facts live inside WIRETXN.cbl; the six cross-file dependencies grey out. A text-window tool structurally cannot see them, so the model emits long TRN_LIMIT and corrupts the database. Flip the toggle back and the graph returns to 9 of 9, with receipts.
Why a bigger context window doesn't close the gap
The intuitive fix is a larger window: put the whole repository in front of the model. It does not survive contact with a real estate. A production codebase runs to one to ten million lines and beyond, and it does not fit in any context window, present or future.
A real codebase never fits in a context window. The hard part is retrieving the exact transitive slice a change touches and proving you found all of it. That is a topology and evidence problem, and a stronger model does not make it go away.
There is a compliance edge to the same point. A regulator asking for a DORA ICT-asset inventory, or a SOC-2 auditor asking which dependencies a change touched, does not want a model's self-report. They want a proof of retrieval. A perfect model still cannot produce that on its own. It still needs a safe extraction order, and it still owes an auditable record of what was resolved and what was not. Those are properties of a graph and a completeness gate, not of the language model sitting on top. Agents advise, code decides.
Proof, not a vibe
The analyses that run on the graph are plain deterministic algorithms, no LLM in the path, so the same fixture yields the same result on every run. On this estate the graph holds 47 nodes and 70 edges across seven node types. The completeness gate resolved 33 of 34 references, 97.1 percent, and did the honest thing with the one it could not: DISPATCH makes a dynamic CALL WS-PROGNAME whose target is computed at runtime (DISPATCH.cbl:15), so it is flagged for review rather than quietly dropped.
The Audit tab. 33 of 34 references resolved, 97.1 percent, with the single unresolvable reference surfaced rather than hidden: DISPATCH's dynamic CALL WS-PROGNAME, a runtime-computed target at DISPATCH.cbl:15, flagged for review.
A completeness gate that hides what it missed is worse than no gate at all. The one reference the graph cannot resolve is the one it is loudest about.
The same graph produces a safe migration order. Each program gets a coupling and blast-radius score, and the estate is ranked so the lowest-risk modules extract first. AUDITLOG comes out first at risk score 0, with zero coupling. The god-program ACCTMGR, coupling 5, extracts last at risk 15. WIRETXN sits mid-pack for its one COMP-3 trap, and DISPATCH ranks near the bottom precisely because of that unresolved dynamic call. All of it exports to a printable Codebase Topology and Completeness Report: the node and edge summary, per-module closures with file:line provenance, the recall result and method, the ranked sequence, and a timestamp. That report is the DORA ICT-asset inventory and the SOC-2 change-control receipt, generated rather than assembled by hand.
The strangler-fig extraction order. Lowest risk extracts first: AUDITLOG at risk 0, the god-program ACCTMGR last at risk 15. The score combines coupling, COMP-3 traps, JCL criticality, and unresolved calls, so a safe migration sequence falls out of the graph instead of a spreadsheet.
Because the fixture is authored, the recall figure is a labeled measurement, not a testimonial. Nine of nine against three of nine describes this wire-transfer scenario against its known ground-truth set. It is not a promise about an arbitrary COBOL estate, and we will not pretend otherwise.
What this is, and what it isn't
CodeGraph is the understanding layer, not a "paste COBOL, get Java" translator. It deliberately does not emit production Java. It builds the map, proves its completeness, and ranks a safe order, and it leaves the writing of code to a downstream step that now has the context it needs. The demo runs the parse, graph, and analyses as deterministic plain Python with no API key and no live mainframe connection: the graph is in-memory plus SQLite, the DB2 and JCL inputs are file fixtures, the naive view is a simulated single-file context, and the optional model-assisted layer is off by default. The estate is synthetic, so there is no real customer, no case study, and no deployment result to report. What the demo proves is the mechanism.
So one honest question for anyone partway through a mainframe modernization: before a single line gets rewritten, can your tooling show you the full transitive slice a change touches, with file:line provenance for every edge, and prove how much of the estate it actually resolved? Or does it read one file at a time and trust the model to have seen the rest? The full run is at https://veriprajna.com/demos/legacy-cobol-modernization, and we would genuinely like to hear where your team draws the line between what the model guesses and what the graph proves.