Loading episodes…
0:00 0:00

Two Layers of Intelligence: Merging Semantic Contracts with Structural Call Graphs

00:00
BACK TO HOME

Two Layers of Intelligence: Merging Semantic Contracts with Structural Call Graphs

10xTeam May 22, 2026 7 min read

tags: [codegraph, contracts, semantic, structural, merged-adapter, call-graph, edges] related:

  • 033_structure-vs-intent.md
  • 036_kill-your-codemap.md
  • 032_pre-index-your-codebase.md status: current —

037 — Two Layers of Intelligence: Merging Semantic Contracts with Structural Call Graphs

When we ran dar contracts build on the DarJS project and looked at the output, something was off. The graph had 263 nodes — one per contract — but almost no edges. The @used-by and @depends-on fields that were supposed to link them were there, but they resolved to almost nothing.

Zero edges. A graph with no connections is not a graph. It is a list.

The contracts system had been working for months. The NLP search was accurate. The coverage gate was enforced. But as a graph, it was empty. Why?

The Problem With Semantic Edges

The @used-by field in a DarJS contract looks like this:

/**
 * @used-by  saveOrder, OrderDetail, InvoiceGenerator
 */
function normalizeOrder(raw) {  }

These are names. To turn them into edges in a graph, you need to resolve the names — find the contract that has name: "saveOrder" and draw a line. That resolution is fuzzy. If the name doesn’t match exactly (different casing, renamed function, abbreviation), the edge is lost.

More fundamentally: @used-by is a human annotation. Someone had to write it. In a project with 263 contracts, most of them don’t have @used-by filled in. The field was required by the contract format, but in practice it defaulted to empty or stayed at whatever was written when the contract was first generated.

The result: a semantically rich node set with no structural connections.

The Problem With Structural Edges

CodeGraph takes the opposite approach. It parses the AST, finds every function call, import, and reference, and records them as edges in SQLite. No human annotation required. No name matching. The edges come from the code.

Running CodeGraph on the same DarJS project:

  • 2,598 nodes — every function, method, class, route, and import
  • 3,941 edges — every call, reference, extends, and implements relationship

The edges are real. runServe calls buildContractsGraph. PageDefRenderer.renderList calls renderTemplate. Every one of those relationships is in the graph, derived from the actual code.

But the nodes are structural, not semantic. CodeGraph knows that runServe is a function at line 104 of contracts.js. It does not know that runServe has the role coordinator, the domain cli, or that you should reach for it “when you need to browse contracts in a browser with live rebuild support.” That knowledge lives in the @contract block. CodeGraph does not read comments.

Two graphs. One has meaning, no connections. One has connections, no meaning.

The Merge

The solution is a MergedAdapter that treats the two sources as complementary layers.

The merge strategy is straightforward:

  1. Use CodeGraph nodes as the base. They cover the whole codebase, not just the 263 contracts.
  2. Enrich with DarJS semantic fields by name match. For every CodeGraph node, look for a contract with the same name. If found, copy role, domain, complexity, does, and reuse-when onto the node.
  3. Use CodeGraph edges exclusively. The 3,941 structural edges replace the 0 resolved @used-by edges entirely.
getNodes() {
  const cgNodes   = this._cg.getNodes();      // 2,598 structural nodes
  const contracts = this._dar.getNodes();      // 263 semantic nodes
  const byName    = new Map(contracts.map(c => [c.name, c]));

  return cgNodes.map(n => {
    const contract = byName.get(n.name);
    return contract
      ? { ...n, role: contract.role, domain: contract.domain,
               complexity: contract.complexity, does: contract.does }
      : n;
  });
}

getEdges() {
  return this._cg.getEdges();  // 3,941 real call edges
}

The result: a graph with 2,042 enriched nodes (after filtering low-signal kinds like import and unresolved_ref) and 1,537 meaningful edges. Nodes that have contracts show their semantic metadata. Nodes that don’t still appear in the graph with their structural data.

Why You Need Both Layers

Each layer answers questions the other cannot.

Semantic layer (contracts) answers:

  • What is this function for? (@does)
  • When should I reach for it? (@reuse-when)
  • What role does it play? (@role — transformer, coordinator, adapter…)
  • How complex is it? (@complexity)
  • What domain does it belong to? (@domain)

Structural layer (CodeGraph) answers:

  • What calls this function?
  • What does this function call?
  • What would break if this changed? (reverse BFS traversal)
  • How connected is this node? (reference count)
  • What is the actual call path from entry point to this symbol?

An AI trying to understand a codebase needs both. “What does runServe do?” is a semantic question. “If I change buildContractsGraph, what else breaks?” is a structural one. Neither layer alone can answer both.

More practically: the semantic layer is where human intent lives. A contract documents what a function is supposed to do, what it’s designed for, when it should be reused. The structural layer is where the code’s actual behavior lives. A call graph documents what the function actually does — what it invokes, what depends on it.

When they agree, you have confidence. When they disagree — a function annotated as a transformer that actually mutates state, a @used-by list that doesn’t match the real callers — you have a signal that something drifted.

The Graph View

The merged graph surfaces this immediately. In the codeview UI, nodes are colored by kind (from CodeGraph) but labeled with role and domain (from the contract). Clicking a node shows its @does description alongside its actual callers and callees.

A node with a contract and high refCount is a hot path in the framework — both semantically important (someone took the time to document it) and structurally important (many things call it). A node with a contract but no callers is either an entry point or dead code.

The combination tells you things neither layer could alone.

The General Principle

Most code intelligence systems pick one layer. Documentation tools (JSDoc, TypeDoc) give you semantic descriptions with no call graph. AST tools (CodeGraph, tree-sitter) give you structural relationships with no semantics. Neither is complete.

If your project has NLP contracts, you already have the semantic layer. Adding an AST indexer costs one npm install and one codegraph init. The merge is twenty lines of JavaScript.

The investment is small. The graph goes from empty to connected. And questions that required reading ten files to answer become single-query lookups.

The insight is not about these specific tools. It is about the gap they reveal: semantic documentation and structural analysis are two different things, they are both necessary, and most projects have at most one of them. The right answer is both, wired together, with each layer doing what it does best.


Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?