Files
rea/docs/javascript-artifact-reconstruction.md
T
c4aaf813e0 fix(javascript): retain selected application paths in Evidence
Record the admitted selected path separately from canonical artifact identity in successful and partial Evidence. Anchor relative inputs consistently across both CLI routes using the existing caller-path owner.

Adapt shared client verifier assertions and boundary coverage from PR #5261, and the dedicated-route anchoring correction proposed in PR #5265. Historical parsing remains independent of live paths, including legacy records without the parameter.

Validation: 118 focused tests in eight files, static and docs checks, discovery, and installed Gemini CLI 0.29.5 loopback verification of 207 schemas/241 examples passed. Synthetic static-analysis fixtures; no live Google API or native target execution.

Fixes #5258

Co-authored-by: kwakhyun <khyun2348@naver.com>
Co-authored-by: vishal-tambi <tambivishal3@gmail.com>
2026-10-20 00:37:45 +08:00

44 KiB

JavaScript artifact reconstruction

Inspect a local Electron/JavaScript directory or ASAR without executing its code. analyze_javascript_application and the CLI routes below return an Evidence record with the application graph, static Electron observations, statistics, limitations, and coverage.

Start with the public workflow, select sources when useful, then read the analysis. Use application workflows for tracing and comparisons, or partial recovery when an operation is interrupted.

Public workflow

The tool reads the caller-selected local directory or ASAR path directly:

rea analyze /absolute/path/to/apps/app.asar --json
rea analyze-javascript-application /absolute/path/to/apps/app.asar \
  --integrity-policy record-and-continue --json

Generic rea analyze selects this static provider for directories and .asar paths when neither --provider nor --snapshot is supplied. The dedicated command and the generic route return their complete results inline. Native targets, single JavaScript files, .app bundles, and explicit deep-provider or snapshot requests retain the native deep-analysis route.

Both CLI routes emit rea_progress JSON lines on stderr as inventory, source parsing, graph construction, validation, and Evidence hashing begin. Stdout remains the selected result document. Completion is reported after Evidence creation; a failed analysis retains its typed diagnostic result.

Configure an MCP client with the ordinary REA setup command:

rea setup

The equivalent MCP input is:

{
  "input_path": "/absolute/path/to/apps/app.asar",
  "format": "auto",
  "integrity_policy": "fail"
}

input_path must be absolute. format accepts auto, asar, or directory. integrity_policy accepts fail or record-and-continue; both CLI routes and the MCP tool default to fail. Evidence parameters retain the caller-selected input_path, including a symlink alias. Both CLI routes anchor relative inputs to their working directory without resolving symlinks. The subject and normalized result use the resolved path for artifact identity; the selected path remains a separate request fact. Older Evidence can omit this parameter; retained records are validated without resolving their paths against the live filesystem. The result retains the canonical local path, root artifact digest, artifact manifest and graph commitments, JavaScript Application Graph, static Electron summary, reconstruction statistics, and explicit limitations. It does not require a live Hopper, Ghidra, browser, or Electron process.

The application graph selects the shallowest package.json outside node_modules as its package root, breaking equal-depth ties by Unicode code-point order. Dependency manifests remain package observations but do not declare Electron main or renderer entries. If only dependency manifests are present, the inventory artifact remains the graph root.

Select sources before analysis

For a question about known entry sources, supply exact artifact-relative file paths in source_paths, or repeat --source-path on either CLI route:

rea analyze /absolute/path/to/app.asar \
  --source-path package.json --source-path main.js --json
{
  "input_path": "/absolute/path/to/app.asar",
  "source_paths": ["package.json", "main.js"],
  "detail": "summary"
}

REA inventories and verifies the complete artifact, including unselected bytes, but allocates source text and performs static analysis only for selected files. Include supporting manifests, HTML or source maps explicitly when needed; imports do not expand the selection. Paths must match relevant inventoried files exactly, including nested ASAR prefixes. Unknown paths, aliases and globs return an invalid_request correction. Selected files are analyzed in request order before unselected scopes are projected. Repeated paths preserve the caller's request and first-occurrence priority, analyzing the file once. Omit the selection for the existing complete application workflow.

The result and summary retain source_selection.requested_paths and source_selection.unselected_files. Every relevant file keeps its original inventory identity; unselected files report text_status: "outside-analysis-scope". Their relationships remain unknown, and application coverage is partial while any file remains outside scope. Selection does not claim whole-application completeness or change integrity policy. Summary detail only reduces delivery; source selection reduces text acquisition and analysis. Explicit native provider or snapshot routing rejects --source-path rather than ignoring it.

Read the analysis

analyze_javascript_application returns the complete analysis Evidence by default. Select "detail": "summary" to retain that complete Evidence in the current session and receive only its inspect_analysis_view summary: artifact identity, statistics, Electron surface counts, application and semantic coverage, limitations, and normalized_result.parent_evidence_id. Pass that ID to inspect_analysis_view for module pages or items, or to the application workflows as a retained reference. The summary does not repeat or truncate the analysis; a server without session retention refuses summary detail with capability_unavailable. CLI output is unchanged and always complete.

With a connected MCP client and the caller-selected absolute inputPath, this recipe analyzes once and pages module identities using the returned ID:

const analysis = await client.callTool({
  name: "analyze_javascript_application",
  arguments: { input_path: inputPath, detail: "summary" },
});
if (analysis.isError) throw new Error(JSON.stringify(analysis.content));
const parentId =
  analysis.structuredContent.normalized_result.parent_evidence_id;
const modules = await client.callTool({
  name: "inspect_analysis_view",
  arguments: {
    source: { kind: "retained-evidence", evidence_id: parentId },
    view: { kind: "page", collection: "modules", offset: 0, limit: 16 },
  },
});
if (modules.isError) throw new Error(JSON.stringify(modules.content));

Select a returned module's exact node_id for an item view. A path can identify several graph roles; module items preserve their recorded properties and source locations. Follow coverage.next_offset when another page matters. For feature tracing, version comparison, and retained-input examples, use the application workflows.

For supplied passive browser or Electron observations, use runtime reconciliation. It compares existing Evidence without rerunning static analysis or contacting CDP.

ASAR integrity

ASAR inventory checks Electron integrity metadata for embedded archive entries and supplied .asar.unpacked companion files. An integrity failure identifies the logical path, declared and calculated SHA-256 values, and whether the entry was unpacked. By default, a mismatch is returned as a failure with its artifact context. Application analysis can explicitly select record-and-continue to analyze observed bytes while retaining each contradiction's declared and observed hashes and observed-untrusted trust. Such results mark application graph coverage partial and include every contradicted path in the limitations. Mismatched nested ASARs stay opaque rather than being expanded.

An unpacked entry whose companion bytes were not supplied remains unavailable. REA continues analyzing embedded JavaScript and records the missing native/resource bytes as unknown. The occurrence keeps hash_status: unavailable; no content-addressed child artifact is created for those missing bytes. See MCP integrity handling for the tool-result contract.

Large results

Use MCP detail: "summary" for a large application, then read selected views of the retained analysis. Summary changes delivery only. To reduce source parsing, select exact source paths.

If a complete response exceeds the transport budget, use its retained Evidence ID for a summary, module page, or item, or export the bundle. See MCP tool results for delivery limits, export, and matching server/client buffer settings. Follow-up results also need to fit that budget. Analyzer limits control analysis memory, source deadlines, and semantic capacity separately.

Recover partial analysis

A source timeout stops the remaining analysis and returns completed facts as partial Evidence. Cancellation after source analysis begins can also retain completed facts, including when interrupted during cleanup, graph construction, or Evidence sealing. Unexamined relationships remain unknown.

Per-file gaps identify the applicable processing phase: JavaScript source analysis uses AST authority, while package/JSON, HTML-reference and source-map processing use artifact-byte authority. File-analysis interruption gaps carry analysis_status: interrupted means the phase was entered (including source-worker startup), and not-started means interruption prevented that phase from beginning. Neither state establishes that parsing completed. Remaining parsers are not run after interruption just to populate a result.

Native add-ons remain metadata-only and acquire no JavaScript parse or analysis gap. This also applies when their paths are outside source_paths; their inventory and caller selection remain available without implying applicable AST work.

MCP returns a partial_observation retained reference after recording succeeds; open it with inspect_analysis_view on the same connection. If the SDK stopped waiting before receiving the reply, follow MCP cancellation recovery to find the record. CLI failure output contains the complete partial Evidence. Owned analyzer cleanup is verified before private files are released; uncertain cleanup retains ownership for retry and is reported separately.

What is reconstructed

The projector reuses the content-addressed artifact inventory and safe artifact readers. It retains canonical artifact-relative paths, exact byte counts, SHA-256 digests, inventory IDs, ASAR container identity, and .asar.unpacked status. Direct ASAR inputs and filesystem-backed ASAR files nested beneath a directory are supported.

JavaScript sources (.js, .jsx, .mjs, and .cjs) and TypeScript sources (.ts, .tsx, .mts, and .cts) are parsed as inert text; analysis does not execute them.

JavaScript and HTML source ranges retain an initial UTF-8 BOM as one UTF-16 code unit, matching the original bytes identified by the artifact digest.

Plain .ts artifact sources use TypeScript syntax without JSX, including angle-bracket type assertions. .tsx and .jsx retain JSX parsing. JavaScript sources use JavaScript grammar with JSX, never TypeScript syntax, so valid JavaScript that TypeScript reads differently keeps its JavaScript meaning.

Relative TypeScript imports can name their emitted .js, .mjs, or .cjs counterparts while resolving to inventoried source files. This substitution requires import or require semantics. Literal Electron preload, renderer, and utility-process runtime paths retain the runtime filename selected by the caller.

Selected bounded text is then parsed as inert data to recover:

  • package.json metadata and declared main or renderer entrypoints;
  • Electron preload paths and renderer files visible in static syntax;
  • local HTML script entrypoints;
  • Webpack and Rspack chunk registrations and module factories represented as AST literals;
  • static imports, dynamic imports, CommonJS require calls, workers, and service workers (only new Worker(new URL(path, import.meta.url)) resolves relative to the calling module; string worker and service-worker paths use the document base URL or Node's working directory, which static analysis does not establish, so they keep their expression and stay unresolved);
  • route, network endpoint, storage, and vendor-marker observations;
  • local source-map declarations and original source names and content digests;
  • explicit BrowserWindow options and webPreferences, including statically resolvable preload entrypoints;
  • contextBridge.exposeInMainWorld and exposeInIsolatedWorld API keys and bounded literal member paths;
  • renderer and main-process IPC operations, literal or dynamic channels, exact handler locations, and conservative pairing status;
  • sender, frame, URL, and origin validation candidates without claiming that a visible check enforces a complete policy;
  • utility-process entrypoints and native .node binding requests without parsing or executing the add-on.

Overloaded .open calls use lexical receiver facts: ambient browser window and document receivers are treated as browsing-context or document operations, while locally shadowed receivers can still contribute network endpoint candidates. A template recovered with a parser error and no cooked value stays dynamic; its raw spelling is not treated as a valid JavaScript string.

RPC client syntax also contributes network endpoint candidates, in both analyze_javascript_application and analyze_web_bundle, distinguished by mechanism:

  • trpc:procedure:<method> records the static member path between the root expression and the method, such as invoice.list in root.invoice.list.useQuery(...). tRPC proxies keep these property names through identifier minification. React hooks and TanStack option factories (useQuery, useMutation, useInfiniteQuery, useSubscription, suspense and prefetch variants, queryOptions, infiniteQueryOptions, mutationOptions, subscriptionOptions) are recognized directly. The vanilla query and mutate methods are recognized on a root bound to createTRPCClient or createTRPCProxyClient in the same lexical binding, or on a path of at least two segments in a source containing TRPCClientError, trpc-accept, or /trpc. Factory bindings must have one unconditional initializer and no binding assignments; shadowed locals do not inherit client identity. subscribe additionally requires a tRPC observer argument (onData, onError, onStarted, onStopped, onComplete, or onConnectionStateChange). this roots, browser globals, and any path containing an RTK Query endpoints segment are never procedures; cache-key helpers such as queryKey issue no request and are excluded.
  • trpc:link:<link> records the literal url option of httpLink, httpBatchLink, httpBatchStreamLink, and httpSubscriptionLink when the link's callee name survives bundling, including (0, module.httpBatchLink) import-call wrappers.
  • graphql:query, graphql:mutation, and graphql:subscription record a named operation found at the start of a string or cooked template literal — the name must be followed by variable definitions (($), a directive, or a selection set starting with a field name — or in a precompiled OperationDefinition document object. A standalone Source object's body remains an operation candidate even when it has locationOffset metadata. A graphql-tag loc.source.body copy is suppressed only when that source belongs to a recovered document with the same operation and its containing document, location and source have proven private metadata ownership. Writes, escapes and classic-script global bindings retain the source observation. Anonymous operations are not reported.

Literal link options and precompiled document fields follow object property order: the final definition owns the key. A later spread, dynamic computed key, method, or accessor can make a field unknown, so an overwritten literal is not reported as the endpoint or operation. A final explicit property after a spread or dynamic key remains usable.

A procedure path is the server-side procedure name, not a resolved URL; the transport URL depends on the link configuration and batching. When the proxy is reached through a member expression, such as a webpack module binding (l.S.invoice.list) or a property holding the proxy (o.api.post.byId), the recorded path keeps those leading carrier segments; the procedure path is its suffix. Utility calls such as useUtils().invoice.list.fetch(), useQueries, and links whose callee was renamed by a minifier are not recognized.

Each recovered bundle module retains the exact factory-source digest. A complete bounded AST also receives a babel-ast-v3 structural fingerprint. It ignores ordinary identifier spellings while retaining literal and directive values, operators, fixed property names, and selected syntax flags such as computed keys, declaration kind, function async/generator state, update-expression prefix, method kind, and class static state. Local names used in computed keys remain rename-resistant; private member names are also ignored while their method kind and static state remain represented.

This fingerprint is structural evidence for candidate matching, not proof that two modules behave equivalently. Its scope follows the AST facts listed above; unrepresented syntax can still collide. The exact source digest remains available to show byte-level change, and comparison reports content changes even when structural matching succeeds.

Version comparison includes the algorithm in its structural matching key. Retained babel-ast-v1 results can still match other v1 results, and v2 results can still match v2 results, but v1/v2 results cannot structurally match v3 results. Other matching tiers remain available across algorithm versions. Re-analyze both snapshots to compare using the v3 rules. If fingerprint construction reaches its structural bound, the fingerprint is unavailable and its status is truncated; a bounded prefix is never presented as a complete stable fingerprint.

Authority and unknowns

Artifact bytes and AST syntax are observations. Entrypoint resolution, imports, loads, calls, and persistence relationships are static inferences and explicitly say that syntax does not prove runtime execution. A malformed or unavailable JavaScript file, package record, or source map produces an unknown graph scope with state: unavailable; it is not treated as evidence that modules or source are absent.

Source-map contents are parsed as local input. The graph stores original source names and optional content digests, not raw sourcesContent text.

Only explicitly present BrowserWindow values are observations. REA does not substitute version-dependent Electron defaults for omitted webPreferences. Unresolved option objects, bridge keys, API objects, and IPC channel expressions remain unknown and make coverage partial.

BrowserWindow options and webPreferences can follow local const aliases whose initialization precedes the use. The shared lexical and mutation index checks the selected webPreferences path, so an unrelated options.icon write does not discard these facts. Reassignment, relevant writes, prior escapes, ambiguous initialization, and unresolved spreads remain unknown. The binding association is an inference: the window observation retains the constructor location and resolution_sources identifies the original object literals in the same artifact. Inline option syntax keeps its observed Evidence. This does not execute code or establish runtime configuration; popup overrides from setWindowOpenHandler are not covered by this constant-options resolution.

Recognized literal module-table bootstraps also support bounded lexical substitution through module exports, import helper bodies, local wrapper calls, returned closures, object spreads, and callback parameters. Recovered Electron paths retain source ranges, module identities, literal channels and branch conditions as static candidates. Unknown spreads, reassigned bindings, accessors, dynamic channels and unsupported syntax do not become exact implementations. This pass neither executes the bundle nor proves that runtime guards pass. An ambiguous or unresolved handler retains its channel registration but does not acquire a path to one arbitrarily selected callback. Unknown factory conditions retain both return branches, including fallthrough. Short-circuit selection distinguishes truthiness from nullishness; uncertain ?? paths retain a nullish guard rather than a falsy guard.

Recognized module factories retain a separate initialization boundary for each reachable return and for fallthrough. A conditional early return does not hide later exports on the continuing path; exports and captured initializers after a definite exit remain unavailable. Fallthrough candidates retain their branch predicates. Exports shared by the complete set of proven exits also retain unconditional facts. Their shared view excludes later overwrites and bounds captured initializers by the earliest completion; branch-dependent facts keep their predicates. Incomplete or additionally guarded exit sets cannot use that proof. Completed factory control flow is reused within that analysis only. Guarded main-process registrations remain candidate paths even with empty callbacks; they do not become unconditional IPC summary facts. Factory guards read export writes at their source position. Destructured and spread snapshots retain their capture position rather than reading later writes. An unproven namespace read boundary remains unknown. Completed factory environments preserve their exports and module.exports alias identity when a namespace escapes through an export or returned closure. Synchronous helper calls retain the caller's factory activation and read point; lexical captures and executing callers remain separate facts. Async calls retain their truthy Promise identity until an explicit await; generator calls produce iterators without evaluating their bodies. Iterator advancement and Promise/iterator members remain explicitly unresolved. Awaited function values retain their source origins and captured initialization bounds.

These paths appear as contextual javascript-expression nodes. An exact api seed such as desktop.copy can trace an exposed member through a unique IPC pairing to an external API call. addListener is normalized to on. Semantic projection retains these boundary paths before bulk function definitions, within the existing per-file node budget; omitted general semantics remain explicitly partial. Wrapper expansion is bounded by one million visits/substitutions and 128 nested substitutions, with exhaustion reported as unknown coverage.

Electron API identity follows proven lexical module aliases. Namespace interop also preserves identity when the complete Rolldown helper operations, lexical roles, live getters, and captured intrinsic origins match, or when a stable helper simply returns its module parameter. Harmless single-statement control blocks preserve recognition; added operations still require proof. Literal member and destructuring projections are retained from proven results. Omitted or literal node-mode arguments preserve named exports; a module-valued default projection requires a truthy literal node mode. Computed or extra arguments remain unresolved. Recognition uses structure rather than emitted names; other helper bodies and ambiguous runtime fallbacks remain unresolved. Captured function identities survive later global-property replacement, including copies through initialized aliases. Reads before an alias initializer remain unresolved. Visible intrinsic writes before capture or conversion are checked in their owning callable scope, including identifier leaves in nested/default/rest destructuring and loop targets. Property keys and shadowed local bindings do not overwrite the global intrinsic identity. Cross-call side effects, including writes inside directly invoked local functions, are not evaluated. Other reassignments, dynamic selections, and visibly overwritten namespace members are unresolved. Known Object/Reflect mutators remain recognizable through literal destructuring of intrinsic objects and stable bound-function aliases. Calls retain bound argument prefixes and the original bound receiver through call, apply, and Reflect.apply, including generic Function.prototype invocation and binding creation. Arguments follow each actual callable invocation rather than property chain depth; rebinding retains the original bound receiver. Visibly replaced invocation intrinsics produce unknown effects instead of using their former call semantics; captured function aliases retain their earlier identity. Unexamined targets remain unresolved. Reflect.set examines its explicit receiver as well as its target, retaining unknown receiver effects. Creating an unused binding does not itself mutate an intrinsic. Variable redeclarations retain a single unconditional initializer when they introduce no assignment or second initializer. Stable identifier chains to fresh object, array, function, undecorated class or RegExp syntax keep those targets separate from intrinsic objects when initialized before use; opaque, decorated, reassigned or not-yet-initialized targets remain unresolved. Writes during class initialization are still retained. Function declarations are available before their textual position; variable and class initializers must precede use. Direct and destructuring assignment, loop, update, and delete targets are checked through namespace aliases; unrelated member writes do not discard observed API identity. This static check does not evaluate arbitrary call side effects or runtime registration. Missing findings do not establish that an application has no Electron bound