Skip to content
Aug 14, 2026·8 min read

How a whole codebase dependency graph finds hidden calls

A whole codebase dependency graph exposes cross-language calls, generated names, batch handoffs, and data contracts that repository scans miss.

How a whole codebase dependency graph finds hidden calls

A dependency graph is only as honest as the boundary used to build it. Scan one repository at a time and the result may look precise, complete, and almost useless: the calls that decide whether a rewrite preserves behavior often cross a scheduler, a database, a generated file, a shared copybook, or a protocol that no repository owns.

Reading the whole tree together changes the unit of analysis. A COBOL program and its JCL are no longer separate assets. An RPG job, a CL wrapper, and a table written by a nightly import become one execution path. The graph can connect evidence that lives in different languages and can also mark where evidence stops. That last part matters. A credible graph distinguishes a proved edge from a plausible one instead of turning every matching name into certainty.

I have seen repository scans produce reassuring diagrams while the production path ran through code that the diagram never touched. The scanner was not broken. Its question was too small.

Repository boundaries are administrative, not behavioral

A per-repository graph describes how source files refer to other source files inside a chosen container. A whole-system graph describes how behavior moves across all artifacts that can affect an execution. Those are different products, and confusing them leads directly to incomplete migration scopes.

Repositories usually reflect team ownership, access rules, vendor drops, or a migration that happened years ago. Runtime behavior does not respect any of those choices. A web request can begin in Classic ASP, invoke a COM component built from VB6, call a stored procedure held in a database repository, and leave work for a scheduled COBOL program. Each repository can be internally well understood while the business transaction remains invisible.

The word dependency also needs discipline. A textual reference is evidence that one artifact names another. A callable dependency means the named target can actually receive control under some condition. A behavioral dependency means changing the target can change an outcome that users or downstream systems observe. Repository tools often collapse all three into a line on a diagram. During a rewrite, that shortcut creates both false confidence and wasted work.

The practical graph needs more than files and functions as nodes. It needs jobs, database objects, message destinations, generated artifacts, external programs, schemas, screens, reports, and configuration entries when those things participate in behavior. An edge must record why it exists: direct call, dynamic dispatch, data flow, scheduling, generation, or an unresolved name. With that evidence attached, an engineer can challenge the graph instead of admiring it.

This wider model does not imply that every file belongs in one repository. Keep the repositories. Change the analysis boundary. The correct boundary is the set of artifacts that can influence the behavior being rewritten, even when ownership and deployment split them apart.

Cross-language edges hide in ordinary mechanisms

The calls that matter rarely announce themselves as cross-language architecture. They appear as ordinary platform mechanisms: a command string, a stored procedure name, a job step, an exported symbol, a queue destination, or a filename produced for the next process. A language parser sees syntax. A system graph has to interpret what that syntax causes elsewhere.

Consider a JCL step with PGM=BILLRUN. The edge does not end at the token BILLRUN. The analyzer must resolve that program through the relevant load libraries, connect it to the source or binary inventory, and preserve uncertainty when several candidates exist. Inside the program, a COBOL CALL WS-PROGRAM is not a normal static call because the target comes from data. The value may originate in a copybook, a parameter file, or a prior database read.

The same pattern appears on other platforms. CL can submit an RPG program to a batch queue. VB6 can create a COM class by a string held in the registry or a configuration file. ColdFusion can call a database procedure whose implementation lives with PL/SQL. A PHP monolith can write a control file that a Perl daemon treats as an instruction. None of these edges requires exotic reflection. They disappear because each parser stops at the edge of its own language or repository.

A useful intermediate artifact makes the join visible and reviewable. One tab-separated edge file can carry the minimum evidence needed for engineers to inspect it:

source	target	mechanism	evidence
JCL:AR_CLOSE:STEP20	COBOL:BILLRUN	program-load	PGM=BILLRUN
COBOL:BILLRUN:PARA140	DB2:SP_POST_LEDGER	dynamic-sql	value-set:POST_LEDGER
DB2:SP_POST_LEDGER	TABLE:GL_ENTRY	write	INSERT INTO GL_ENTRY
CL:ENDDAY:CMD7	RPG:RECONCILE	submit-job	CALL PGM(RECONCILE)

That file is not the final graph. It is a contract between extraction and review. Every row states the source, the target, the mechanism, and the evidence. If the tool cannot populate the last column, it should not silently draw a hard edge.

Names become resolvable only when evidence is pooled

Dynamic calls are not inherently unknowable. Many become resolvable when the analyzer pools assignments, configuration, build metadata, and runtime samples from across the tree. The mistake is treating a nonliteral call site as a dead end before looking for the values that can reach it.

Suppose a COBOL paragraph calls WS-NEXT-PGM. One repository contains the call but not the assignment. JCL in another repository passes a symbolic parameter. A shared copybook defines the field width. A control table export contains the deployed values. Read separately, the call is unresolved. Read together, the analyzer can derive a candidate set, reject values that cannot fit the field, and connect each surviving value to a program inventory.

Resolution should remain evidence based. I use four edge states in review: confirmed by syntax, derived from bounded values, observed in recorded traffic, and unresolved. The names are less important than refusing to merge the states. A derived candidate is useful for scope, but it is not proof that production exercises that path. An observed call proves occurrence in the sample, but it does not prove that other targets cannot occur.

This is also where naive name matching goes wrong. POST, UPDATE, or CLOSE may appear in dozens of namespaces. A match becomes credible only after applying the platform's resolution rules, field constraints, calling convention, deployment context, and reachable assignments. Whole-tree analysis supplies more evidence, but it must also supply stricter disambiguation. More input without rules simply produces a denser wrong graph.

Generated source deserves the same treatment. The generator, its templates, its inputs, and the emitted artifact form a chain. If only generated output is scanned, the graph mistakes a consequence for an authority. If only templates are scanned, it misses the concrete names created for a deployment. Keep both and mark the generation edge, so a change can be traced to the thing that will recreate the file later.

Data movement is often the missing call

A call graph alone cannot explain many legacy systems because data is the handoff. One process writes a row, file, or spool entry; another process interprets it later. No function directly calls the next function, yet the first program controls what the second one does. For behavioral parity, that is a dependency.

Nightly processing makes this obvious. An online transaction writes a status code to a table. A scheduler starts a batch job at close. The job selects rows with that code, creates a fixed-width file, and a separate program imports the file into a ledger. Repository-local call graphs show four disconnected islands. The system graph shows a transaction whose edges include a predicate, a schedule, a record layout, and a file naming convention.

Treating every shared table as a dependency would create noise. The graph needs operation and field sensitivity. A writer that changes customer.last_seen may have no behavioral relationship with a reader that selects customer.credit_hold. A writer that changes the selected status field does. Likewise, two programs touching the same file are not necessarily connected if they use different record types.

A practical data edge records at least the object, operation, relevant fields or record layout, and any predicate that gates the reader. For batch files, include the producer, consumer, naming rule, encoding, delimiter or field positions, and the control totals checked at ingestion. For stored procedures, distinguish calling the procedure from reading tables that the procedure changes. These details decide where a rewrite can safely introduce a new schema or service boundary.

The awkward question is whether this makes the graph too broad to use. It does if every data coincidence becomes a hard edge. It stays useful when the graph supports filtered views: control transfer, data influence, scheduling, generation, and external uncertainty. The unified model should preserve the kinds of edges, not flatten them into one mass of arrows.

A complete tree still has an outside

Replace hidden batch handoffs
Cross-language analysis exposes scheduled programs and file-driven behavior before the new architecture is chosen.

Reading every file in the supplied tree does not produce complete knowledge of the running system. It produces complete knowledge of that evidence set. External schedulers, database triggers, operator commands, registry entries, middleware routing, vendor binaries, and production configuration can still introduce behavior that source analysis cannot prove.

This distinction is where many modernization reports become dishonest. They label a graph complete when they mean the scanner finished. Completion of a scan says nothing about whether the input covered the execution environment. A strong result includes an explicit frontier: nodes and edges that point beyond the supplied evidence, with a reason each one remains open.

You can make that frontier concrete. Export unresolved program names, external database objects, queue destinations, file paths, and configuration keys into a review table. Give each item an owner and a disposition such as supplied later, verified external, retired, or still unknown. Do not delete an unresolved node because an engineer does not recognize it. Old systems often contain dormant code, but recognition is not reachability evidence.

Runtime observations help, provided nobody calls them exhaustive. Recorded production traffic can confirm that a path occurs and supply values for dynamic targets. It cannot prove that a rare year-end branch, recovery procedure, or operator-only command never runs. Static evidence gives possible structure; runtime evidence gives witnessed behavior. Their overlap is strong evidence, and their disagreement is where engineers should investigate.

The graph should therefore answer two separate questions: what can the supplied artifacts cause, and what did the recorded workload actually cause? A migration scope based only on the first may preserve dead paths forever. A scope based only on the second may delete a valid exception path. Keep both views and make the decision visible.

One missed edge can invalidate a clean rewrite

A missed dependency usually fails far from the code that was omitted. That delay is why teams underestimate repository boundaries during planning and then blame test coverage when the cutover exposes the real system.

Take a month-end close assembled from four repositories. An RPG program marks eligible accounts and calls a CL wrapper. The wrapper submits a job using a name read from a data area. JCL on a connected host runs the selected COBOL program. That program writes a fixed-width exception file, and a VB6 desktop tool lets an operator approve records before a PL/SQL procedure posts them. Each repository has tests. Each team can explain its own segment.

A repository scan finds the direct RPG-to-CL call and the PL/SQL table writes. It misses the submitted program because the name is data, the JCL because it lives elsewhere, and the desktop approval because the file is the interface. The rewrite replaces the first segment with a service and reproduces its database updates. Automated tests pass against normal accounts.

At close, exception accounts remain unposted. The new service never emits the fixed-width file because nobody included that output in its contract. The desktop tool has nothing to display, so the operator cannot approve records. No exception reaches the posting procedure. The defect looks like a UI or database problem, but the omission happened when the analysis defined the service boundary around one repository.

A whole-tree graph would connect the path through different edge types: call, submitted job, scheduled program, file production, operator action, and stored procedure invocation. It would also identify the data area and filename rule as control inputs. Tests could then record representative traffic on both normal and exception paths and compare observable outputs at each boundary.

The lesson is not that every legacy artifact deserves preservation. The lesson is that deletion must follow understanding. Once the path is visible, the team may replace the file and desktop approval with a TypeScript client and a service API. That is architecture change with a known behavioral obligation, rather than accidental removal.

Transliterating the graph preserves the wrong boundaries

Handle the million-line tree
The agentic platform reads codebases over a million lines with every language processed in parallel.

Generating one new module for every old program feels safe because the mapping is easy to audit. It is also how a rewrite inherits decades of deployment accidents. The graph should preserve obligations and reveal better boundaries, not dictate a file-for-file translation.

Old boundaries often exist because of memory limits, batch windows, language restrictions, or team history. A JCL step may merely move data between layouts because neither adjacent program could own both formats. A stored procedure may contain business rules because the original client could not deploy often. A shared copybook may couple unrelated programs because it was the only practical distribution mechanism. Recreating each artifact in a new language keeps those constraints alive without their original reason.

Use the graph to find cohesive behavior instead. Nodes that change together, share transactional rules, and participate in the same observable outcome are candidates for one modern component. Edges that cross trust zones, independent release cycles, or genuinely different workloads are candidates for explicit interfaces. Data edges show where a Postgres schema must protect invariants. Numeric kernels with strict performance or memory needs may warrant Rust, while orchestration can sit in Go and operator-facing work in TypeScript. The target should follow the behavior and constraints, not the source file extensions.

I argue against beginning with a repository-by-repository conversion even when procurement and staffing make it attractive. It produces early progress that is easy to count, but it defers cross-repository behavior until integration, when changing boundaries costs more. Build the system graph first, choose the new boundaries, and then decide how work packages map to teams.

Traceability remains essential. Every new component should point back to the source behaviors and graph edges it replaces. That map lets reviewers ask whether an old edge was preserved, intentionally redesigned, or retired with evidence. Without it, modernization becomes a debate over code resemblance, which is the wrong measure.

Parity tests must follow paths, not repositories

Repository test suites are useful evidence, but they rarely establish system parity because their assertions stop at local boundaries. A parity harness should replay complete transactions and compare the observable effects along the path that the graph exposed.

Start by selecting behaviorally distinct paths, not merely popular endpoints. Include normal processing, a dynamically selected target, a batch handoff, an operator-mediated exception, and a failure or retry path when the system has them. Record inputs and outputs at stable boundaries: requests, database changes, emitted records, status transitions, reports, and externally visible errors. Internal call sequences can change with the architecture. Observable obligations cannot change by accident.

A compact path manifest makes the test scope reviewable:

path: close-exception-approval
entry: account-status-change
observations:
  - eligible-account-row
  - exception-record
  - approval-state
  - ledger-entry
dynamic-targets:
  - reconciliation-program
external-frontier:
  - scheduler-calendar

For each recorded production case, run the original and replacement against controlled state, normalize values that legitimately vary, and compare the observations. A different timestamp may be acceptable. A missing exception record is not. When results differ, use the dependency path to locate the first divergent boundary rather than comparing millions of lines or staring at the final database state.

Traffic samples need deliberate supplementation. Production recordings reflect what happened during the capture window, so engineers should add cases for calendar boundaries, permissions, recovery, rare control values, and operator actions identified by static analysis. Conversely, if static analysis finds an apparently reachable branch that no sample exercises, do not silently discard it. Resolve whether the path is dormant, inaccessible, or simply rare.

CodeHero reads the whole codebase across its languages and checks the rewritten system with a parity harness against recorded production traffic, delivering the project in under 30 days. The useful standard is still the same for any approach: every claimed replacement boundary should have graph evidence behind it and behavioral evidence across it.

The graph is a reviewable engineering artifact

Resolve the dynamic targets
Whole-tree analysis pools assignments and calling context instead of dropping nonliteral program names.

A whole codebase dependency graph earns trust when engineers can inspect its evidence, reproduce its extraction, and record decisions on uncertain edges. A beautiful diagram without provenance is a presentation, not an engineering artifact.

Keep the raw edge inventory under version control with stable identifiers for artifacts and locations. Record the extractor version and the input revision. When an edge comes from inference, store the bounded values and rules that produced it. When runtime traffic confirms an edge, attach a sample identifier rather than pasting sensitive payloads into the graph. This makes graph changes explainable as the source tree and migration design evolve.

Review should focus on boundaries and uncertainty. Ask which externally visible outcomes cross repository lines, which dynamic targets remain unresolved, which data edges control later behavior, and which outside systems can inject work. A giant central diagram is a poor review surface. Filter the same graph into a path for one transaction, an unresolved frontier, a write-to-read influence view, or the proposed target component map.

Ownership matters after discovery. Assign unresolved edges to people who can obtain scheduler exports, database definitions, production configuration, or operator procedures. Record the answer in the graph rather than in meeting notes. If an artifact is declared dead, preserve the evidence for that decision, such as unreachable configuration values and absence from a sufficiently representative traffic set. Silence is not proof of retirement.

The first useful deliverable is not a count of files or an impressive node total. It is a small set of end-to-end paths with every transition backed by evidence and every unknown exposed. Those paths let architects draw boundaries, let testers select cases, and let operators recognize missing steps. Expand until the behaviors in scope have that treatment.

Build and deployment rules decide which edge is real

Source names alone cannot tell you which implementation runs. Build scripts, link maps, library search orders, package manifests, deployment descriptors, and environment-specific overrides decide which candidate receives control. A whole-tree graph that ignores those artifacts may connect the right name to the wrong code.

Duplicate names are normal in long-lived systems. A test version may sit beside production source. Two load libraries may contain programs with the same member name. A VB6 project can reference a compatible COM interface while deployment selects a particular registered implementation. Database synonyms can point the same SQL text at different schemas. The graph needs a deployment context, not a universal answer that pretends every candidate runs at once.

Model resolution as a sequence of evidence. First collect the call-site name and calling convention. Then apply the build or runtime search order for the selected environment. Check whether the candidate exports the expected entry point and accepts a compatible shape. Finally, attach the configuration or deployment record that selected it. If one of those inputs is missing, retain the candidates and expose the missing decision instead of choosing whichever file was indexed first.

Linker and compiler outputs often settle questions that source parsing cannot. A link map can show the symbol bound into a binary. Build logs can show which generated source and copybook versions were used. Package and deployment inventories can connect a built artifact to the host where recorded traffic reached it. These outputs are not secondary paperwork. They are evidence about the executable system, and they should receive stable nodes or attachments in the graph.

Keep environment differences visible. Development, test, disaster recovery, and production may resolve the same logical name differently. Collapsing those variants can hide a production-only dependency or persuade testers that they exercised code which their environment never loaded. Tag edges with their applicable context and compare contexts directly. A difference may be intended, but it still belongs in scope when the rewrite must support that environment.

This work also catches stale declarations. A build file may name a library that no longer ships, while the deployed binary resolves everything it needs elsewhere. Conversely, source may appear unused because the current build definition omits it, even though an operator compiles it through a separate procedure before an annual run. Do not decide between those stories by preference. Record the conflicting evidence and obtain the missing build or operating procedure.

The test for an edge is simple: another engineer should be able to follow its evidence and reach the same candidate set under the same environment assumptions. Reproducibility matters more than forcing a single answer. A graph that shows two defensible targets and one missing configuration file is more useful than a crisp arrow based on directory proximity.

Version identity also matters after the first graph is built. A dependency recorded against a source path can drift when a release branch, copied library, or generated member changes without the path changing. Store a content digest or build identity beside the node, then connect the deployed executable to that exact revision. Otherwise a reviewer may follow perfectly valid evidence into the wrong version and approve a replacement for behavior that never ran. This is especially important when teams supply archives assembled from several machines rather than a clean checkout. The analyzer should report duplicate paths, inconsistent timestamps, and artifacts whose build identity cannot be tied to source. Those warnings do not block discovery, but they prevent the graph from claiming more precision than the inventory supports. When a later export supplies the missing binary or build log, the stable identity lets the team update the affected edges without rebuilding every decision from meeting notes.

Per-repository analysis can still help a team understand local code. It cannot establish the behavioral boundary of a system assembled across languages, jobs, data stores, generated artifacts, and human actions. If the calls that matter cross the containers used for analysis, the containers have already decided what the graph will miss.

FAQ

What is a whole codebase dependency graph?

It is a graph built from every relevant source, configuration, job, schema, generated artifact, and observed runtime path in the system scope. Its edges keep their mechanisms and evidence, so a direct call does not look identical to a data handoff or an inferred dynamic target.

Why does repository-level dependency analysis miss calls?

Repositories encode ownership and delivery choices, while execution crosses those boundaries through schedulers, databases, files, queues, and dynamic names. A scanner that cannot see both ends can record an unresolved token at best and often drops the edge entirely.

Can static analysis resolve dynamic program calls?

It can resolve many of them to a bounded candidate set by tracing assignments, parameters, configuration, field constraints, and platform resolution rules. It should label that result as derived evidence rather than pretending it is a confirmed literal call.

Should database reads and writes appear in a dependency graph?

Yes, when they carry behavior between components. Record the operation, relevant fields, and reader predicate, because two programs touching the same table do not automatically influence each other.

Does reading the entire source tree find every dependency?

No. External schedulers, production configuration, operator procedures, triggers, and vendor binaries can sit outside the tree, so the graph needs an explicit unresolved frontier. Runtime observations and environment exports can close some of those gaps.

How do runtime traces improve a dependency graph?

They confirm that specific paths and dynamic target values occurred in recorded traffic. They do not prove that unobserved paths are dead, so combine them with static reachability and targeted cases for rare operations.

How should generated code be represented?

Keep the generator, templates, inputs, and emitted files as separate nodes connected by generation edges. That prevents engineers from editing output that will be overwritten and preserves the concrete names used in a deployment.

Can a dependency graph define new service boundaries?

It can provide the evidence for that decision by showing cohesive behavior, transactional rules, data influence, and true external seams. Do not turn every old program or repository into a service merely because the mapping is convenient.

What should a parity harness compare during a rewrite?

Compare observable effects across complete behavior paths: responses, database changes, emitted records, status transitions, reports, and errors. Normalize only values that may legitimately differ, then investigate the first divergent boundary.

How can engineers review a graph with millions of lines of source?

Review filtered paths and uncertainty views rather than one enormous diagram. Stable edge records with mechanisms, locations, and evidence let teams inspect one transaction, unresolved target set, or proposed component boundary at a time.