6.1

analyze-javascript-application and the application graph

rea parses the six files of ledger-desk without running them and returns one derived record whose graph has 42 nodes and 59 edges, each with its own authority.

You have a small Electron-style application, six files in capture/fixtures/js-app/. You want the map before you read code: which renderer talks to which main-process handler, and what leaves the machine. The analysis answers that without Ghidra and without running a line of the application. When you finish this section, you can run the analysis, read the record and its graph, and page through a large result with inspect-analysis-view.

The command and the record

The CLI command is rea analyze-javascript-application PATH. The plain rea analyze PATH reaches the same code. It picks this workflow for a directory or an .asar file when you give neither --provider nor --snapshot.

The inputs are input_path, format as auto, asar, or directory, and integrity_policy. The optional source_paths input names exact inventoried files. rea then analyzes only those files, and the relationships of the other files stay unknown.

Four resource limits are optional. max_heap_mb is 1024 MiB by default, and analysis_timeout_ms is 300,000 ms by default. max_semantic_nodes is 100,000 by default, and max_semantic_nodes_per_file is 20,000. Over MCP, you can also set detail to complete or summary. The parser is @babel/parser. It runs with errorRecovery on, and it does not attach comments.

the head of the analyze_javascript_application recordjsonFull log
{
  "evidence_id": "ev_14bd1d1e…",
  "subject": {
    "name": "js-app",
    "digest": {
      "sha256": "bb9f726c…"
    },
    "local_path": "$CAPTURE/fixtures/js-app",
    "format": "directory",
    "architecture": null
  },
  "provider": {
    "id": "rea-javascript-application",
    "name": "REA JavaScript application analyzer",
    "version": "1"
  },
  "analysis_profile": null,
  "predicate_type": "rea.javascript-application-analysis",
  "operation": "analyze_javascript_application",
  "parameters": {
    "format": "auto",
    "integrity_policy": "fail"
  },
The kit kept the record without graph and semantic_graph, with --filter-output. The result fields follow.

This is an Evidence record like the ones in Part 5, with three differences from a Ghidra dossier. The provider is rea itself, rea-javascript-application version 1. The confidence is derived, because rea computed relationships from syntax, and the authority is shipped-artifact, because the six files on disk are the source. The subject is the directory, with the SHA-256 bb9f726c… of its inventory as the digest. The normalized_result has eleven fields: input_path, format, root_artifact_sha256, inventory_manifest_id, inventory_graph_sha256, integrity_contradictions, graph, summary, statistics, limitations, and semantic_graph. A twelfth field, source_selection, is present only when the request names source_paths.

the summary and the statistics of ledger-deskjsonFull log
"summary": {
  "browser_windows": 1,
  "explicit_web_preferences": 3,
  "preload_entrypoints": 1,
  "context_bridge_apis": 1,
  "exposed_api_members": 3,
  "ipc": {
    "operations": 6,
    "literal_channels": 3,
    "dynamic_channel_operations": 0,
    "renderer_transmissions": 3,
    "renderer_listeners": 0,
    "main_handlers": 3,
    "paired_renderer_transmissions": 3,
    "ambiguous_renderer_transmissions": 0,
    "unpaired_literal_renderer_transmissions": 0
  },
  …
},
"statistics": {
  "relevant_files": 6,
  "nested_asar_containers": 0,
  "text_bytes_read": 2888,
  "invalid_utf8_files": 0,
  "parsed_javascript_files": 4,
  "skipped_javascript_files": 0,
  "visited_ast_nodes": 424,
  "findings": 25,
  "modules": 0,
  "parse_failures": 0
},
The utility-process and native-addon counts, all zero, are cut.

Check the numbers against the source in capture/fixtures/js-app/. main.js creates one BrowserWindow with three explicit webPreferences, preload, contextIsolation, and nodeIntegration. preload.js exposes one contextBridge API, ledger, with three members. The six IPC operations are the two invoke and one send in preload.js, and the two handle and one on in main.js. They use three literal channels, and each channel is paired.

Four of the six files are JavaScript, 424 AST nodes were visited, and no file failed to parse. skipped_javascript_files is 0, so no JavaScript file was left out for size or for a resource limit. When that count is above 0, the graph is incomplete, and the count tells you by how many files. modules counts bundler modules, and this application has no bundle. While the command ran, rea wrote nineteen progress events to stderr, from inventory to the final hash of the record. The events also show the split between the files. rea reads index.html and package.json for metadata only, and it parses the four JavaScript files.

five of the nineteen progress eventstextFull log
[stderr] {"rea_progress":{"phase":"analyze_javascript_application","completed":0,"total":1,"message":"Inventorying and parsing application artifacts","sequence":1}}
…
[stderr] {"rea_progress":{"phase":"parse_javascript_sources","completed":0,"total":1,"message":"Processing 6 selected application files","sequence":3}}
[stderr] {"rea_progress":{"phase":"process_javascript_artifact_metadata","completed":0,"total":1,"message":"Processing metadata for index.html (1/6)","sequence":4}}
[stderr] {"rea_progress":{"phase":"parse_javascript_source","completed":0,"total":1,"message":"Parsing and projecting ledger.js (2/6)","sequence":5}}
…
[stderr] {"rea_progress":{"phase":"analyze_javascript_application","completed":1,"total":1,"message":"Application graph and Electron boundaries reconstructed","terminal":true,"sequence":19}}
Events 1, 3, 4, 5, and 19 of the first analyze-javascript-application call. The other fourteen are cut.

The graph

Application graph: the provider-neutral model of an application as nodes and directed edges, with an evidence block on every edge. The documentation fixes two rules. The graph is built from syntax alone. It reads no files and makes no CDP calls, it does not run JavaScript, and it has no node kinds for one application.

The edges use a fixed set of directed relations: contains, loads, imports, maps_to, exposes, sends, invokes, handles, calls, persists_to, observed_as, and changed_from.

The graph for ledger-desk in 15-js-app-graph.json has 42 nodes and 59 edges. Its own coverage is complete, and it omits nothing. Each of the four source modules records the SHA-256 of its own file as source_sha256. That digest is the one 00-fixtures.txt lists for the file, so you can match a module to its bytes without the directory digest.

Node kindCountIn ledger-desk
javascript-module11the four files as modules, plus electron, node:path, and ./ledger as they are required
javascript-expression9three steps (exposure, closure, terminal call) for each of the three bridge members ledger.percentChange, ledger.gradeLabel, and ledger.log
javascript-asset4main.js, preload.js, renderer.js, ledger.js by content digest
artifact3the root directory, package.json, index.html
ipc-channel3ledger:percent-change, ledger:grade-label, ledger:log
ipc-handler3the two ipcMain.handle and the one ipcMain.on in main.js
electron-renderer2index.html, seen from loadFile and from the HTML reference
package, electron-main, browser-window, electron-preload, context-bridge-api, endpoint, storage1 eachledger-desk, main.js, BrowserWindow@8:17-16:4, preload.js, ledger, /api/ledger/latest, ledger:latest
RelationCountEvidence on the edges
contains197 observed from artifact bytes, 8 observed from the AST, 4 inferred
imports11inferred
maps_to8inferred
loads5inferred
calls4inferred: one fetch to the endpoint, and one step edge in each of the three bridge call paths
invokes4inferred
handles3inferred
sends, exposes2 eachinferred
persists_to1inferred, localStorage.setItem

Every edge says how it was obtained. An AST fact has authority ast-static-analysis, state observed, confidence exact, and a source range. A relationship has authority static-relationship-inference, state inferred, and confidence high by default. It always carries the limitation Static syntax does not prove that this relationship executes at runtime. The documentation states the rule for the Electron edges: loads, exposes, invokes, handles, and imports stay static inferences.

one invokes edge, from preload.js to the handler in main.jsjsonFull log
{
  "source_node_id": "jag_node_c239425e…",
  "target_node_id": "jag_node_36721459…",
  "relation": "invokes",
  "properties": {
    "channel": "ledger:grade-label",
    "pairing_basis": "unique-exact-literal-channel",
    "handler_path": "main.js",
    "handler_operation": "handle"
  },
  "evidence": {
    "authority": "static-relationship-inference",
    "state": "inferred",
    "confidence": "high",
    …
    "location": {
      "available": true,
      "value": {
        "kind": "source-range",
        "source": "preload.js",
        "start": {
          "line": 7,
          "column": 25
        },
        "end": {
          "line": 7,
          "column": 72
        }
      }
    },
    …
The artifact reference, the extractor, the coverage, and the limitations of the edge are cut.

Line 7 of preload.js, columns 25 to 72, is ipcRenderer.invoke("ledger:grade-label", score). The edge pairs this call with the ipcMain.handle in main.js, because the channel is one exact literal that appears once on each side. It is inferred, not observed, because nothing ran. The same pairing would be ambiguous if two handlers used the channel, and dynamic channel names get no pairing edge at all. Figure 6.1 draws the graph with the file names and channel names from the capture.

Fig. 6.1the application graph of ledger-desktree
The application graph of ledger-desk The graph that analyze_javascript_application returns for the js-app fixture, drawn as boxes with the relation names on the arrows. Row one: the package ledger-desk loads the electron-main main.js, which contains the BrowserWindow at lines 8 to 16. Row two: main.js loads the electron-renderer index.html, the BrowserWindow loads the electron-preload preload.js, and preload.js exposes the context-bridge-api ledger with three members. Row three: preload.js invokes the ipc-channels ledger:percent-change and ledger:grade-label and sends on ledger:log. Row four: three ipc-handlers in main.js handle those channels. Row five: index.html loads the javascript-asset renderer.js, which calls the network endpoint /api/ledger/latest with fetch and persists_to the storage ledger:latest with localStorage.setItem. ledger-desk package, package.json main.js electron-main BrowserWindow@8:17-16:4 browser-window loads contains index.html electron-renderer ledger context bridge, 3 members preload.js electron-preload loads loads exposes ledger:percent-change ipc-channel ledger:grade-label ipc-channel ledger:log ipc-channel sends invokes ledger:percent-change handler, ipcMain.handle ledger:grade-label handler, ipcMain.handle ledger:log handler, ipcMain.on handles handles handles renderer.js javascript-asset /api/ledger/latest endpoint, fetch ledger:latest storage, localStorage loads calls persists_to
The graph links the package entry point to three IPC handlers and one network endpoint, and each edge names whether it was observed or inferred. Read from the top left. Olive boxes are the application's own code, the grey box is the network endpoint outside it, and the teal box is browser storage. Relation names sit on the arrows. The nine javascript-expression step nodes of the three bridge call paths are not drawn.

Views over a large record

Six files give a record of about 200 KB. The documentation warns that for a real application the complete Evidence can reach hundreds of megabytes.

inspect-analysis-view reads a view out of a record that already exists, without running the analysis again. Its input is a source and a view. The source is the complete record inline, or a reference to a record the MCP session retains. The view is a summary, a page of a collection, or one module. The capture asked for the summary and for the first 32 items of the modules collection.

Each view is an Evidence record of its own, from provider rea-analysis-view. The parent id is in parameters.parent_evidence_id and in evidence_links, so the chain from view to analysis is explicit.

the coverage block of the summary viewjsonFull log
"coverage": {
  "application": {
    "status": "complete",
    "truncated": false,
    "omitted_count": 0,
    "limits": []
  },
  …
  "application_unknowns": 0,
  "semantic_unknowns": 118
},
"limitation_count": 7
The twelve semantic families, all partial, are cut.

The application graph is complete. The semantic graph, the second graph in the record, is not. Its coverage status is unknown, each of its twelve families is partial, and 118 semantic unknowns remain. The view lists that fact among its unknowns as semantic_graph.coverage.status is unknown. This is the rule from residual unknowns in a second shape. A graph that says it is partial is more useful than a graph that says nothing.

the modules page, items and coveragejsonFull log
"items": [
  {
    "node_id": "jag_node_0fc7e361…",
    "kind": "javascript-asset",
    "path": "main.js"
  },
  {
    "node_id": "jag_node_19e7e901…",
    "kind": "javascript-module",
    "path": "main.js"
  },
  …
"coverage": {
  "status": "page",
  "examined": 15,
  "total": 15,
  "next_offset": null,
  "exhausted": true
},
Thirteen of the fifteen items are cut.

A page says how much of the collection it covers: 15 of 15 items here, with no next offset. For a record with thousands of modules you would read next_offset and ask again. One last detail ties this section to the Evidence record. Both view records carry the application path inside normalized_result and locations. The masked $CAPTURE in the capture therefore changes their ids, and your own run will show different ids for the same views.

rea (rea-agents) with the Ghidra provider rea-agents 6.7.0, Ghidra 12.1.4 · 27dc42d · 2026-10-18 · Edition 2026.10