From be6863e9a16bcc3a09d87c50981e69292795a512 Mon Sep 17 00:00:00 2001
From: kvalls <95490470+kvalls@users.noreply.github.com>
Date: Fri, 9 Oct 2026 16:11:06 +0100
Subject: [PATCH 01/74] feat(ghidra): support native PE DLLs on Windows
---
.github/workflows/real-ghidra-windows.yml | 20 +++
AGENTS.md | 2 +-
docs/installation.md | 2 +-
docs/managed-code-analysis.md | 5 +-
docs/provider-evaluation.md | 9 +-
docs/roadmap.md | 2 +-
docs/windows-ghidra-p0.md | 15 +-
scripts/create-ghidra-windows-fixture.mjs | 155 +++++++++++++--------
scripts/verify-packaged-ghidra-windows.mjs | 114 ++++++++++++---
scripts/verify-real-ghidra-windows.mjs | 27 +++-
src/ghidra/GhidraAnalysisProfile.ts | 6 +
src/ghidra/GhidraProvider.behavior.test.ts | 84 ++++++-----
src/ghidra/GhidraProvider.ts | 9 +-
src/ghidra/GhidraProviderCapabilities.ts | 2 +-
14 files changed, 316 insertions(+), 136 deletions(-)
diff --git a/.github/workflows/real-ghidra-windows.yml b/.github/workflows/real-ghidra-windows.yml
index 42dc38fa46..d6de7469f7 100644
--- a/.github/workflows/real-ghidra-windows.yml
+++ b/.github/workflows/real-ghidra-windows.yml
@@ -64,6 +64,14 @@ jobs:
env:
REA_ANALYSIS_PROVIDER: ghidra
run: npm run verify:ghidra:windows -- --x86 | Tee-Object -FilePath windows-ghidra-x86-proof.log
+ - name: Verify native x64 DLL Windows Ghidra operations
+ env:
+ REA_ANALYSIS_PROVIDER: ghidra
+ run: npm run verify:ghidra:windows -- --dll | Tee-Object -FilePath windows-ghidra-dll-proof.log
+ - name: Verify native x86 DLL Windows Ghidra operations
+ env:
+ REA_ANALYSIS_PROVIDER: ghidra
+ run: npm run verify:ghidra:windows -- --x86 --dll | Tee-Object -FilePath windows-ghidra-x86-dll-proof.log
- name: Verify ordinary-user CLI and MCP operations
env:
REA_ANALYSIS_PROVIDER: ghidra
@@ -72,6 +80,14 @@ jobs:
env:
REA_ANALYSIS_PROVIDER: ghidra
run: npm run verify:ghidra:windows:package -- --x86 | Tee-Object -FilePath windows-ghidra-x86-package-proof.log
+ - name: Verify ordinary-user x64 DLL CLI and MCP operations
+ env:
+ REA_ANALYSIS_PROVIDER: ghidra
+ run: npm run verify:ghidra:windows:package -- --dll | Tee-Object -FilePath windows-ghidra-dll-package-proof.log
+ - name: Verify ordinary-user x86 DLL CLI and MCP operations
+ env:
+ REA_ANALYSIS_PROVIDER: ghidra
+ run: npm run verify:ghidra:windows:package -- --x86 --dll | Tee-Object -FilePath windows-ghidra-x86-dll-package-proof.log
- uses: actions/upload-artifact@v4
if: always()
with:
@@ -81,4 +97,8 @@ jobs:
windows-ghidra-package-proof.log
windows-ghidra-x86-proof.log
windows-ghidra-x86-package-proof.log
+ windows-ghidra-dll-proof.log
+ windows-ghidra-x86-dll-proof.log
+ windows-ghidra-dll-package-proof.log
+ windows-ghidra-x86-dll-package-proof.log
retention-days: 30
diff --git a/AGENTS.md b/AGENTS.md
index 1acb825527..01ecf3d91d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,7 +2,7 @@
## Product Direction
-REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64/arm64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86 and x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers.
+REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64/arm64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86 and x86-64 PE applications and DLLs; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers.
Prioritize:
diff --git a/docs/installation.md b/docs/installation.md
index a0be329205..dfa3bc84bf 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -452,7 +452,7 @@ entry comments atomically and returns refreshed analysis. These session metadata
edits leave executable bytes unchanged and are discarded on close. GUI controls
require Hopper; Windows P0 remains read-only.
-Windows P0 admits native x86 and x86-64 PE applications on fixed local NTFS volumes.
+Windows P0 admits native x86 and x86-64 PE applications and DLLs on fixed local NTFS volumes.
The npm package bundles native Job Object ownership, protected private runtime
DACLs, and handle-based path admission; no separate addon installation is needed.
See the [Windows Ghidra P0 guide](windows-ghidra-p0.md) for verified scope and
diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md
index 92bc230609..bb87a7a635 100644
--- a/docs/managed-code-analysis.md
+++ b/docs/managed-code-analysis.md
@@ -125,8 +125,9 @@ The upstream analyzer currently gates its own analysis to x86-64. NativeAOT's
broader platform matrix does not establish REA coverage: PE/COFF, ELF, Mach-O,
shared libraries, target architectures, and runtime metadata versions each need
their own provider verification. REA's experimental Windows Ghidra boundary is
-limited to admitted native x86 and x86-64 PE applications, so it does not currently
-establish coverage for PE DLLs. Mobile NativeAOT targets should remain
+limited to admitted native x86 and x86-64 PE applications and DLLs for static
+analysis. This does not establish Windows NativeAOT metadata-recovery coverage.
+Mobile NativeAOT targets should remain
experimental until REA has a matching provider and package-level verification.
For REA's native analysis, preserve the same artifact path and digest used by
diff --git a/docs/provider-evaluation.md b/docs/provider-evaluation.md
index 8aa655e52a..a18303a92b 100644
--- a/docs/provider-evaluation.md
+++ b/docs/provider-evaluation.md
@@ -18,7 +18,7 @@ comments atomically, verifies readback, and returns refreshed function analysis.
Changes live only in the session database and leave executable bytes unchanged.
Ghidra does not expose GUI controls; Windows P0 remains read-only.
-Windows x64 P0 supports native x86 and x86-64 PE applications on local NTFS with
+Windows x64 P0 supports native x86 and x86-64 PE applications and DLLs on local NTFS with
bundled Job Object ownership, protected runtime DACLs, and handle-based path
admission. Real ordinary-user verification covers all 25 read-only operations
through the packaged CLI and MCP, target integrity, and cleanup. This remains
@@ -62,7 +62,7 @@ maximum, when the release leaves those at the 12.1 defaults). Otherwise doctor
probes `java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Supported Linux and macOS
hosts accept compatible ELF, PE, and Mach-O executable targets. Host admission
and target compatibility are separate checks. Windows x64 P0 admits only native,
-non-managed, non-DLL x86 and x86-64 PE applications on fixed local NTFS, with the packaged
+non-managed x86 and x86-64 PE applications and DLLs on fixed local NTFS, with the packaged
native authority available before launch.
The launcher creates one ephemeral runtime root with project,
@@ -136,8 +136,9 @@ Mach-O target coverage. It requires `clang`, LLD, and `lld-link`; set
these tools before compilation. Keeping this matrix separate lets Linux
host/provider acceptance run with only the host compiler.
-`npm run verify:ghidra:windows` and its `-- --x86` variant check the source-owned native x86 and x86-64 PE
-fixture, all 25 read-only operations, target/snapshot/import SHA-256 linkage,
+`npm run verify:ghidra:windows` and its `-- --x86` variant check source-owned native x86 and x86-64 PE
+applications. Adding `-- --dll` or `-- --x86 --dll` selects entry-point-free DLLs
+with exported caller/callee functions. These lanes check all 25 read-only operations, target/snapshot/import SHA-256 linkage,
and project, endpoint, process, and runtime cleanup. The independent native
lane checks DACLs, handle admission, cancellation, and Job Object lifecycle.
`npm run verify:ghidra:windows:package` packs and installs REA into an isolated
diff --git a/docs/roadmap.md b/docs/roadmap.md
index b4ac0ba545..077efa6325 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -87,7 +87,7 @@ and exact commands.
Experimental Windows x64 P0 now supplies bundled Job Object ownership,
protected runtime DACLs, and handle-based admission for native x86 and x86-64 PE
-applications on local NTFS. Real ordinary-user CLI/MCP verification covers
+applications and DLLs on local NTFS. Real ordinary-user CLI/MCP verification covers
all 25 read-only operations and cleanup. The [Windows P0 guide](windows-ghidra-p0.md)
describes this boundary and unverified broader coverage.
diff --git a/docs/windows-ghidra-p0.md b/docs/windows-ghidra-p0.md
index ec54f5908a..5f50d832c2 100644
--- a/docs/windows-ghidra-p0.md
+++ b/docs/windows-ghidra-p0.md
@@ -17,7 +17,7 @@ additional permission flag or degraded mode is required.
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+;
- an operator-installed Ghidra 12.1.x distribution (verified with 12.1.4);
- a 64-bit full JDK inside that installation's declared Java range (JDK 21 or newer, with no maximum, for current 12.1 releases);
-- an explicit native, non-managed, non-DLL x86 or x86-64 PE application; and
+- an explicit native, non-managed x86 or x86-64 PE application or DLL; and
- the 25 read-only Ghidra inventory, memory, and function-analysis operations.
Loaded memory reads and file offsets preserve Ghidra's observed source mappings.
@@ -38,6 +38,13 @@ REA selects its packaged bridge scripts by explicit path. Same-named files or
directories in the caller's working directory do not select a different
script or require users to clean that directory before analysis.
+Native DLL analysis preserves the PE shared-library role in the analysis profile.
+REA imports an immutable copy for static analysis; it does not load the library
+into the analyst process or execute its entry routine. The source-owned DLL
+fixtures have no entry routine or imports and expose two exported functions.
+The same native snapshot, path-admission and cleanup requirements apply to EXEs
+and DLLs.
+
## Configuration
```powershell
@@ -100,7 +107,7 @@ and artifact SHA-256 before loading the package-owned addon. Native failure
reasons remain distinct from a valid installation and an unsupported target.
PE header classification and SHA-256 come from the same open file. The Windows
-provider admits native x86 and x86-64 PE applications and rejects unsupported roles,
+provider admits native x86 and x86-64 PE applications and DLLs and rejects unsupported roles,
architectures, managed images, and malformed headers. The original selected
source coordinate is preserved for native admission.
@@ -159,7 +166,11 @@ artifact and manifest.
npm run verify:windows-native
npm run verify:ghidra:windows
npm run verify:ghidra:windows -- --x86
+npm run verify:ghidra:windows -- --dll
+npm run verify:ghidra:windows -- --x86 --dll
npm run verify:ghidra:windows:package -- --x86
+npm run verify:ghidra:windows:package -- --dll
+npm run verify:ghidra:windows:package -- --x86 --dll
npm run verify:ghidra:windows:package -- C:\fixtures\installed-rea C:\fixtures\sample.exe
```
diff --git a/scripts/create-ghidra-windows-fixture.mjs b/scripts/create-ghidra-windows-fixture.mjs
index 6538ec57f8..e610b46880 100644
--- a/scripts/create-ghidra-windows-fixture.mjs
+++ b/scripts/create-ghidra-windows-fixture.mjs
@@ -3,68 +3,103 @@ import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+// Source-owned static fixtures: no compiler, target loading or execution.
for (const architecture of ["x86_64", "x86"]) {
- const x86 = architecture === "x86";
- const output = resolve(
- root,
- "build",
- "fixtures",
- x86 ? "rea-ghidra-windows-x86.exe" : "rea-ghidra-windows.exe",
- );
- const bytes = Buffer.alloc(1024);
- const peOffset = 0x80;
- const optionalHeader = peOffset + 24;
- const optionalHeaderSize = x86 ? 0xe0 : 0xf0;
- const sectionTable = optionalHeader + optionalHeaderSize;
+ for (const dll of [false, true]) {
+ const x86 = architecture === "x86";
+ const output = resolve(
+ root,
+ "build",
+ "fixtures",
+ `rea-ghidra-windows${x86 ? "-x86" : ""}.${dll ? "dll" : "exe"}`,
+ );
+ const bytes = Buffer.alloc(1024);
+ const peOffset = 0x80;
+ const optionalHeader = peOffset + 24;
+ const optionalHeaderSize = x86 ? 0xe0 : 0xf0;
+ const sectionTable = optionalHeader + optionalHeaderSize;
+ const directories = optionalHeader + (x86 ? 96 : 112);
- bytes.write("MZ", 0, "ascii");
- bytes.writeUInt32LE(peOffset, 0x3c);
- bytes.write("PE\0\0", peOffset, "binary");
- bytes.writeUInt16LE(x86 ? 0x14c : 0x8664, peOffset + 4);
- bytes.writeUInt16LE(1, peOffset + 6);
- bytes.writeUInt16LE(optionalHeaderSize, peOffset + 20);
- bytes.writeUInt16LE(x86 ? 0x102 : 0x22, peOffset + 22);
+ bytes.write("MZ", 0, "ascii");
+ bytes.writeUInt32LE(peOffset, 0x3c);
+ bytes.write("PE\0\0", peOffset, "binary");
+ bytes.writeUInt16LE(x86 ? 0x14c : 0x8664, peOffset + 4);
+ bytes.writeUInt16LE(1, peOffset + 6);
+ bytes.writeUInt16LE(optionalHeaderSize, peOffset + 20);
+ bytes.writeUInt16LE(
+ (x86 ? 0x102 : 0x22) | (dll ? 0x2000 : 0),
+ peOffset + 22,
+ );
- bytes.writeUInt16LE(x86 ? 0x10b : 0x20b, optionalHeader);
- bytes.writeUInt32LE(0x200, optionalHeader + 4);
- bytes.writeUInt32LE(0x1000, optionalHeader + 16);
- bytes.writeUInt32LE(0x1000, optionalHeader + 20);
- if (x86) bytes.writeUInt32LE(0x400000, optionalHeader + 28);
- else bytes.writeBigUInt64LE(0x1_4000_0000n, optionalHeader + 24);
- bytes.writeUInt32LE(0x1000, optionalHeader + 32);
- bytes.writeUInt32LE(0x200, optionalHeader + 36);
- bytes.writeUInt16LE(6, optionalHeader + 40);
- bytes.writeUInt16LE(6, optionalHeader + 48);
- bytes.writeUInt32LE(0x2000, optionalHeader + 56);
- bytes.writeUInt32LE(0x200, optionalHeader + 60);
- bytes.writeUInt16LE(3, optionalHeader + 68);
- bytes.writeUInt16LE(x86 ? 0x8140 : 0x8160, optionalHeader + 70);
- if (x86) {
- bytes.writeUInt32LE(0x10_0000, optionalHeader + 72);
- bytes.writeUInt32LE(0x1000, optionalHeader + 76);
- bytes.writeUInt32LE(0x10_0000, optionalHeader + 80);
- bytes.writeUInt32LE(0x1000, optionalHeader + 84);
- bytes.writeUInt32LE(16, optionalHeader + 92);
- } else {
- bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 72);
- bytes.writeBigUInt64LE(0x1000n, optionalHeader + 80);
- bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 88);
- bytes.writeBigUInt64LE(0x1000n, optionalHeader + 96);
- bytes.writeUInt32LE(16, optionalHeader + 108);
+ bytes.writeUInt16LE(x86 ? 0x10b : 0x20b, optionalHeader);
+ bytes.writeUInt32LE(0x200, optionalHeader + 4);
+ bytes.writeUInt32LE(dll ? 0 : 0x1000, optionalHeader + 16);
+ bytes.writeUInt32LE(0x1000, optionalHeader + 20);
+ if (x86) bytes.writeUInt32LE(0x400000, optionalHeader + 28);
+ else bytes.writeBigUInt64LE(0x1_4000_0000n, optionalHeader + 24);
+ bytes.writeUInt32LE(0x1000, optionalHeader + 32);
+ bytes.writeUInt32LE(0x200, optionalHeader + 36);
+ bytes.writeUInt16LE(6, optionalHeader + 40);
+ bytes.writeUInt16LE(6, optionalHeader + 48);
+ bytes.writeUInt32LE(0x2000, optionalHeader + 56);
+ bytes.writeUInt32LE(0x200, optionalHeader + 60);
+ bytes.writeUInt16LE(3, optionalHeader + 68);
+ bytes.writeUInt16LE(x86 ? 0x8140 : 0x8160, optionalHeader + 70);
+ if (x86) {
+ bytes.writeUInt32LE(0x10_0000, optionalHeader + 72);
+ bytes.writeUInt32LE(0x1000, optionalHeader + 76);
+ bytes.writeUInt32LE(0x10_0000, optionalHeader + 80);
+ bytes.writeUInt32LE(0x1000, optionalHeader + 84);
+ bytes.writeUInt32LE(16, optionalHeader + 92);
+ } else {
+ bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 72);
+ bytes.writeBigUInt64LE(0x1000n, optionalHeader + 80);
+ bytes.writeBigUInt64LE(0x10_0000n, optionalHeader + 88);
+ bytes.writeBigUInt64LE(0x1000n, optionalHeader + 96);
+ bytes.writeUInt32LE(16, optionalHeader + 108);
+ }
+
+ bytes.write(".text", sectionTable, "ascii");
+ bytes.writeUInt32LE(dll ? 0x180 : 0x16, sectionTable + 8);
+ bytes.writeUInt32LE(0x1000, sectionTable + 12);
+ bytes.writeUInt32LE(0x200, sectionTable + 16);
+ bytes.writeUInt32LE(0x200, sectionTable + 20);
+ bytes.writeUInt32LE(0x6000_0020, sectionTable + 36);
+
+ // EXE entry returns zero. DLL caller adds one to the callee's 42.
+ bytes.set(
+ dll
+ ? [0xe8, 0x0b, 0, 0, 0, 0x83, 0xc0, 1, 0xc3]
+ : [0xe8, 0x0b, 0, 0, 0, 0x31, 0xc0, 0xc3],
+ 0x200,
+ );
+ bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210);
+
+ if (dll) {
+ // Export both functions; an entry-point-free library is discovered by exports.
+ bytes.writeUInt32LE(0x1040, directories);
+ bytes.writeUInt32LE(0x130, directories + 4);
+ const exports = 0x240;
+ bytes.writeUInt32LE(0x1100, exports + 12);
+ bytes.writeUInt32LE(1, exports + 16);
+ bytes.writeUInt32LE(2, exports + 20);
+ bytes.writeUInt32LE(2, exports + 24);
+ bytes.writeUInt32LE(0x1080, exports + 28);
+ bytes.writeUInt32LE(0x1088, exports + 32);
+ bytes.writeUInt32LE(0x1090, exports + 36);
+ bytes.writeUInt32LE(0x1000, 0x280);
+ bytes.writeUInt32LE(0x1010, 0x284);
+ bytes.writeUInt32LE(0x1120, 0x288);
+ bytes.writeUInt32LE(0x1140, 0x28c);
+ bytes.writeUInt16LE(0, 0x290);
+ bytes.writeUInt16LE(1, 0x292);
+ bytes.write("rea-fixture.dll\0", 0x300, "binary");
+ bytes.write("fixture_caller\0", 0x320, "binary");
+ bytes.write("fixture_return42\0", 0x340, "binary");
+ }
+
+ await mkdir(dirname(output), { recursive: true });
+ await writeFile(output, bytes);
+ if (!x86 && !dll) process.stdout.write(`${output}\n`);
}
-
- bytes.write(".text", sectionTable, "ascii");
- bytes.writeUInt32LE(0x16, sectionTable + 8);
- bytes.writeUInt32LE(0x1000, sectionTable + 12);
- bytes.writeUInt32LE(0x200, sectionTable + 16);
- bytes.writeUInt32LE(0x200, sectionTable + 20);
- bytes.writeUInt32LE(0x6000_0020, sectionTable + 36);
-
- // entry calls the second function, returns zero; second returns 42.
- bytes.set([0xe8, 0x0b, 0, 0, 0, 0x31, 0xc0, 0xc3], 0x200);
- bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210);
-
- await mkdir(dirname(output), { recursive: true });
- await writeFile(output, bytes);
- if (!x86) process.stdout.write(`${output}\n`);
}
diff --git a/scripts/verify-packaged-ghidra-windows.mjs b/scripts/verify-packaged-ghidra-windows.mjs
index 17d29c4ada..268d949d9e 100644
--- a/scripts/verify-packaged-ghidra-windows.mjs
+++ b/scripts/verify-packaged-ghidra-windows.mjs
@@ -15,7 +15,7 @@ import {
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
-import { pathToFileURL } from "node:url";
+import { fileURLToPath, pathToFileURL } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
@@ -24,10 +24,43 @@ if (process.platform !== "win32" || process.arch !== "x64")
throw new Error(
"Packaged Windows Ghidra acceptance requires a Windows x64 host.",
);
-const workspace = await mkdtemp(join(tmpdir(), "rea-packaged-ghidra-"));
const exec = promisify(execFile);
-const x86Fixture = process.argv[2] === "--x86";
-const packageRootArgument = x86Fixture ? undefined : process.argv[2];
+const arguments_ = process.argv.slice(2);
+// Windows locks the loaded package-owned addon until the verifier exits.
+// Keep installation cleanup in a parent that never loads that addon.
+if (arguments_[0] !== "--internal-workspace") {
+ const ownedWorkspace = await mkdtemp(join(tmpdir(), "rea-packaged-ghidra-"));
+ try {
+ const result = await exec(
+ process.execPath,
+ [
+ fileURLToPath(import.meta.url),
+ "--internal-workspace",
+ ownedWorkspace,
+ ...arguments_,
+ ],
+ { timeout: 900_000, maxBuffer: 8 * 1024 * 1024 },
+ );
+ process.stdout.write(result.stdout);
+ process.stderr.write(result.stderr);
+ } catch (cause) {
+ if (typeof cause.stdout === "string") process.stdout.write(cause.stdout);
+ if (typeof cause.stderr === "string") process.stderr.write(cause.stderr);
+ throw cause;
+ } finally {
+ await rm(ownedWorkspace, { recursive: true, force: true });
+ }
+ process.exit(0);
+}
+assert.ok(arguments_[1] !== undefined, "Missing verifier workspace.");
+const workspace = resolve(arguments_[1]);
+const fixtureArguments = arguments_.slice(2);
+const x86Fixture = fixtureArguments.includes("--x86");
+const dllFixture = fixtureArguments.includes("--dll");
+const [packageRootArgument, targetArgument, reportArgument] =
+ fixtureArguments.filter(
+ (argument) => argument !== "--x86" && argument !== "--dll",
+ );
let packageRoot = resolve(packageRootArgument ?? ".");
// The default lane verifies the npm artifact in an isolated prefix. Explicit
// package roots support an already-installed artifact without a second install.
@@ -75,17 +108,17 @@ if (packageRootArgument === undefined) {
);
packageRoot = join(prefix, "node_modules", "rea-agents");
} catch (cause) {
- await rm(workspace, { recursive: true, force: true });
- throw cause;
+ throw new Error("Packaged Windows verifier installation failed.", {
+ cause,
+ });
}
}
const target = resolve(
- process.argv[3] ??
- (x86Fixture
- ? "build/fixtures/rea-ghidra-windows-x86.exe"
- : "build/fixtures/rea-ghidra-windows.exe"),
+ targetArgument ??
+ `build/fixtures/rea-ghidra-windows${x86Fixture ? "-x86" : ""}.${dllFixture ? "dll" : "exe"}`,
);
let TOOL_CONTRACTS, native, token, sha256;
+let parsedTarget;
try {
({ TOOL_CONTRACTS } = await import(
pathToFileURL(join(packageRoot, "dist/contracts/toolContracts.js"))
@@ -107,9 +140,25 @@ try {
);
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
sha256 = digest(await readFile(target));
+ const { parseBinaryTarget } = await import(
+ pathToFileURL(join(packageRoot, "dist/application/BinaryTargetResolver.js"))
+ );
+ parsedTarget = await parseBinaryTarget(target);
+ assert.equal(parsedTarget.ok, true);
+ assert.equal(parsedTarget.value.format, "pe");
+ assert.equal(parsedTarget.value.managed, false);
+ if (targetArgument === undefined) {
+ assert.equal(
+ parsedTarget.value.architecture,
+ x86Fixture ? "x86" : "x86_64",
+ );
+ assert.equal(
+ parsedTarget.value.executableRole,
+ dllFixture ? "shared-library" : "application",
+ );
+ }
} catch (cause) {
- await rm(workspace, { recursive: true, force: true });
- throw cause;
+ throw new Error("Packaged Windows verifier prerequisites failed.", { cause });
}
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
const runtimeParent = join(workspace, "runtime with spaces");
@@ -145,6 +194,8 @@ const report = {
architecture: "x64",
nonAdmin: true,
targetSha256: sha256,
+ targetArchitecture: parsedTarget.value.architecture,
+ targetRole: parsedTarget.value.executableRole,
cli: {},
mcpOperations: [],
nativeControls: "available",
@@ -208,8 +259,8 @@ try {
});
const call = async (name, arguments_) => {
report.lastOperation = name;
- if (process.argv[4] !== undefined)
- await writeFile(process.argv[4], `${JSON.stringify(report)}\n`);
+ if (reportArgument !== undefined)
+ await writeFile(reportArgument, `${JSON.stringify(report)}\n`);
const contract = TOOL_CONTRACTS.find((value) => value.name === name);
assert.ok(contract !== undefined, `Missing canonical contract: ${name}`);
const argumentsParsed = contract.inputSchema.parse(arguments_);
@@ -230,6 +281,25 @@ try {
await call("list_names", {});
const procedures = await call("list_procedures", {});
assert.ok(procedures.length > 0);
+ if (targetArgument === undefined) {
+ const calleeAddress = x86Fixture ? "0x401010" : "0x140001010";
+ const calleeCode = await call("procedure_pseudo_code", {
+ procedure: calleeAddress,
+ });
+ assert.match(calleeCode, /\b(?:42|0x2a)\b/iu);
+ if (dllFixture) {
+ const callerAddress = x86Fixture ? "0x401000" : "0x140001000";
+ assert.ok(
+ (
+ await call("procedure_callees", { procedure: callerAddress })
+ ).includes(calleeAddress),
+ );
+ assert.match(
+ await call("procedure_pseudo_code", { procedure: callerAddress }),
+ /\+\s*1\b/u,
+ );
+ }
+ }
assert.ok((await call("list_segments", {})).length > 0);
await call("list_strings", {});
const procedure =
@@ -296,6 +366,12 @@ try {
"Packaged verification omitted an admitted operation.",
);
const session = await call("binary_session", {});
+ assert.equal(
+ session.analysis_provider_binding?.analysis_profile?.parameters
+ ?.executable_role,
+ parsedTarget.value.executableRole,
+ "MCP profile must preserve the selected PE role.",
+ );
assert.equal(
session.analysis_provider_binding?.selection_source,
"auto-single-candidate",
@@ -327,8 +403,8 @@ try {
report.callerScriptCollisionsPreserved = true;
report.runtimeCleanup = true;
report.ok = true;
- if (process.argv[4] !== undefined)
- await writeFile(process.argv[4], `${JSON.stringify(report)}\n`);
+ if (reportArgument !== undefined)
+ await writeFile(reportArgument, `${JSON.stringify(report)}\n`);
process.stdout.write(`${JSON.stringify(report)}\n`);
} catch (cause) {
report.failure = {
@@ -338,9 +414,7 @@ try {
.replaceAll(target, "")
.replaceAll(workspace, ""),
};
- if (process.argv[4] !== undefined)
- await writeFile(process.argv[4], `${JSON.stringify(report)}\n`);
+ if (reportArgument !== undefined)
+ await writeFile(reportArgument, `${JSON.stringify(report)}\n`);
throw cause;
-} finally {
- await rm(workspace, { recursive: true, force: true });
}
diff --git a/scripts/verify-real-ghidra-windows.mjs b/scripts/verify-real-ghidra-windows.mjs
index 1c5b3b829a..3332ea4e2d 100644
--- a/scripts/verify-real-ghidra-windows.mjs
+++ b/scripts/verify-real-ghidra-windows.mjs
@@ -62,21 +62,21 @@ if (
);
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
-const architecture = process.argv[2] === "--x86" ? "x86" : "x86_64";
+const architecture = process.argv.includes("--x86") ? "x86" : "x86_64";
+const dllFixture = process.argv.includes("--dll");
+const role = dllFixture ? "shared-library" : "application";
const targetPath = resolve(
root,
"build",
"fixtures",
- architecture === "x86"
- ? "rea-ghidra-windows-x86.exe"
- : "rea-ghidra-windows.exe",
+ `rea-ghidra-windows${architecture === "x86" ? "-x86" : ""}.${dllFixture ? "dll" : "exe"}`,
);
const target = await parseBinaryTarget(targetPath);
if (
!target.ok ||
target.value.format !== "pe" ||
target.value.architecture !== architecture ||
- target.value.executableRole !== "application" ||
+ target.value.executableRole !== role ||
target.value.managed !== false
)
throw new Error(
@@ -89,6 +89,8 @@ const profile = await resolveGhidraAnalysisProfile(
);
if (!profile.ok || profile.value.profile === null)
throw new Error("Windows Ghidra profile could not be committed");
+if (profile.value.profile.parameters.executable_role !== role)
+ throw new Error("Windows Ghidra profile lost the observed PE role");
const client = new GhidraClient({
platform: installation.platform,
@@ -162,6 +164,19 @@ try {
throw new Error(
"Windows fixture decompilation lost its known return value",
);
+ if (dllFixture) {
+ const callerAddress = architecture === "x86" ? "0x401000" : "0x140001000";
+ const callees = await functionOperation("procedure_callees", {
+ procedure: callerAddress,
+ });
+ if (!callees.includes(callee.address))
+ throw new Error("DLL export caller lost its controlled call reference");
+ const callerCode = await functionOperation("procedure_pseudo_code", {
+ procedure: callerAddress,
+ });
+ if (typeof callerCode !== "string" || !/\+\s*1\b/u.test(callerCode))
+ throw new Error("DLL export caller lost its addition of one");
+ }
await inventory("inspect_native_load_image", {});
const memory = await inventory("read_bytes", {
@@ -254,7 +269,7 @@ try {
limitations: [
"approved-non-sensitive-fixtures-only",
"windows-x64-local-ntfs-only",
- "native-x86-and-x86-64-pe-applications-only",
+ "native-x86-and-x86-64-pe-applications-and-dlls-only",
"no-gui-or-mutation-authority",
],
};
diff --git a/src/ghidra/GhidraAnalysisProfile.ts b/src/ghidra/GhidraAnalysisProfile.ts
index 1af7469fa0..eeb8646a41 100644
--- a/src/ghidra/GhidraAnalysisProfile.ts
+++ b/src/ghidra/GhidraAnalysisProfile.ts
@@ -34,6 +34,12 @@ export const resolveGhidraAnalysisProfile = (
profile: createAnalysisProfile(provider, {
target_kind: target.kind,
target_format: target.format,
+ ...(target.format === "pe"
+ ? {
+ executable_role: target.executableRole ?? null,
+ managed: target.managed ?? null,
+ }
+ : {}),
architecture: target.architecture ?? null,
available_architectures: [
...(target.availableArchitectures ?? []),
diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts
index 7bbbc8c89d..88e1eb07fe 100644
--- a/src/ghidra/GhidraProvider.behavior.test.ts
+++ b/src/ghidra/GhidraProvider.behavior.test.ts
@@ -270,7 +270,7 @@ describe("Ghidra platform support", () => {
expect(
ghidra.inspectTargetSupport({
...nativeApplication,
- executableRole: "shared-library",
+ executableRole: "non-executable",
}),
).toMatchObject({
status: "unsupported",
@@ -295,45 +295,59 @@ describe("Ghidra platform support", () => {
code: "architecture_unsupported",
});
});
- it("admits generated PE32 applications while rejecting unsupported x86 roles", async () => {
+});
+
+describe("Ghidra Windows PE fixture admission", () => {
+ it("admits generated PE32 and PE32+ DLLs and applications while preserving native-only Windows admission", async () => {
const root = fileURLToPath(new URL("../../", import.meta.url));
await promisify(execFile)(process.execPath, [
join(root, "scripts/create-ghidra-windows-fixture.mjs"),
]);
- const path = join(root, "build/fixtures/rea-ghidra-windows-x86.exe");
- const bytes = await readFile(path);
- const metadata = parseExecutableHeader(bytes, "x64");
- expect(metadata).toMatchObject({
- ok: true,
- value: {
- format: "pe",
- architecture: "x86",
- executableRole: "application",
- managed: false,
- },
- });
- if (!metadata.ok) throw new Error(metadata.error);
- if (metadata.value.format !== "pe")
- throw new Error("Expected a PE fixture");
- const target: BinaryTarget = {
- path,
- sha256: createHash("sha256").update(bytes).digest("hex"),
- kind: "executable",
- ...metadata.value,
- };
const ghidra = provider({ ...installationHost(), platform: "win32" });
- expect(ghidra.inspectTargetSupport(target).status).toBe("supported");
- for (const [candidate, code] of [
- [
- { ...target, executableRole: "shared-library" },
- "target_role_unsupported",
- ],
- [{ ...target, managed: true }, "managed_target_unsupported"],
- ] as const)
- expect(ghidra.inspectTargetSupport(candidate)).toMatchObject({
- status: "unsupported",
- code,
- });
+ for (const architecture of ["x86", "x86_64"] as const) {
+ for (const dll of [false, true]) {
+ const path = join(
+ root,
+ "build/fixtures",
+ `rea-ghidra-windows${architecture === "x86" ? "-x86" : ""}.${dll ? "dll" : "exe"}`,
+ );
+ const bytes = await readFile(path);
+ const metadata = parseExecutableHeader(bytes, "x64");
+ const role = dll ? "shared-library" : "application";
+ expect(metadata).toMatchObject({
+ ok: true,
+ value: {
+ format: "pe",
+ architecture,
+ executableRole: role,
+ managed: false,
+ },
+ });
+ if (!metadata.ok) throw new Error(metadata.error);
+ if (metadata.value.format !== "pe")
+ throw new Error("Expected a PE fixture");
+ const target: BinaryTarget = {
+ path,
+ sha256: createHash("sha256").update(bytes).digest("hex"),
+ kind: "executable",
+ ...metadata.value,
+ };
+ expect(ghidra.inspectTargetSupport(target).status).toBe("supported");
+ for (const [candidate, code] of [
+ [
+ { ...target, executableRole: "non-executable" },
+ "target_role_unsupported",
+ ],
+ [{ ...target, managed: true }, "managed_target_unsupported"],
+ [{ ...target, architecture: "arm64" }, "architecture_unsupported"],
+ ] as const) {
+ expect(ghidra.inspectTargetSupport(candidate)).toMatchObject({
+ status: "unsupported",
+ code,
+ });
+ }
+ }
+ }
});
});
diff --git a/src/ghidra/GhidraProvider.ts b/src/ghidra/GhidraProvider.ts
index d657a156e3..d782827d96 100644
--- a/src/ghidra/GhidraProvider.ts
+++ b/src/ghidra/GhidraProvider.ts
@@ -247,12 +247,15 @@ const inspectWindowsP0TargetSupport = (
reason: "Windows Ghidra P0 accepts x86 and x86-64 PE targets only.",
diagnostics,
};
- if (target.executableRole !== "application")
+ if (
+ target.executableRole !== "application" &&
+ target.executableRole !== "shared-library"
+ )
return {
status: "unsupported",
code: "target_role_unsupported",
reason:
- "Windows Ghidra P0 accepts PE applications, not DLL or non-executable images.",
+ "Windows Ghidra P0 accepts PE applications and DLLs, not non-executable or unclassified images.",
diagnostics,
};
if (target.managed !== false)
@@ -260,7 +263,7 @@ const inspectWindowsP0TargetSupport = (
status: "unsupported",
code: "managed_target_unsupported",
reason:
- "Windows Ghidra P0 accepts native PE applications; managed or unclassified PE targets are unsupported.",
+ "Windows Ghidra P0 accepts native PE applications and DLLs; managed or unclassified PE targets are unsupported.",
diagnostics,
};
return {
diff --git a/src/ghidra/GhidraProviderCapabilities.ts b/src/ghidra/GhidraProviderCapabilities.ts
index edbbb11c31..5b9a6ca8ba 100644
--- a/src/ghidra/GhidraProviderCapabilities.ts
+++ b/src/ghidra/GhidraProviderCapabilities.ts
@@ -31,7 +31,7 @@ export const healthLimitations = Object.freeze([
/** Additional limitations applied to the experimental Windows x64 P0 boundary. */
export const windowsP0Limitations = Object.freeze([
- "Windows Ghidra P0 accepts approved native x86 and x86-64 PE applications only; DLL, managed, hostile, sensitive, and mutable-path targets are unsupported.",
+ "Windows Ghidra P0 accepts approved native x86 and x86-64 PE applications and DLLs; managed, hostile, sensitive, and mutable-path targets are unsupported.",
"The Windows bridge uses authenticated IPv4 loopback because Node path-based IPC does not expose Java AF_UNIX sockets; the endpoint file contains no bearer token.",
"Windows sessions require the matching packaged Windows x64 native addon, local NTFS targets and runtimes, and Windows 10 or later. Native handles enforce path admission, private DACLs, and Job Object ownership automatically.",
]);
From e10c934d5a756c1a4c7b547ebd02c403ddfde7b8 Mon Sep 17 00:00:00 2001
From: GRAB Racem
Date: Sat, 10 Oct 2026 01:36:11 +0200
Subject: [PATCH 02/74] fix(ghidra): keep the cross-format branch callee
multi-block
The cross-format oracle requires a callee whose recovered control flow
has more than one basic block, and rea_cross_branch provided it through
a symmetric if/else of tail calls. Clang 21 folds that shape into a
conditional negate plus one tail branch, a single basic block, so the
lane rejected a correct Ghidra analysis of the fixture.
Give the branch a loop whose trip count depends on a volatile global:
no compiler can eliminate the backedge, and the callee stays multi-block
at every optimization level on every toolchain.
---
tests/conformance/ghidra/cross-format.c | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/tests/conformance/ghidra/cross-format.c b/tests/conformance/ghidra/cross-format.c
index f718b9b540..22efa61597 100644
--- a/tests/conformance/ghidra/cross-format.c
+++ b/tests/conformance/ghidra/cross-format.c
@@ -8,10 +8,15 @@ __attribute__((noinline, used)) int rea_cross_leaf(int value) {
}
__attribute__((noinline, used)) int rea_cross_branch(int value) {
- if (value > 10) {
- return rea_cross_leaf(value);
+ /* A volatile-dependent loop keeps this callee multi-block at every
+ optimization level: newer clang folds a symmetric if/else of tail calls
+ into a conditional negate, and cross-format verification requires a
+ recovered multi-block callee. */
+ int adjusted = value > 10 ? value + 1 : -value;
+ while (adjusted > 100) {
+ adjusted -= rea_cross_global;
}
- return rea_cross_leaf(-value);
+ return rea_cross_leaf(adjusted);
}
__attribute__((noinline, used)) int rea_cross_indirect(
From 40f632a155bff2defdea09e467229c703bc0b0e9 Mon Sep 17 00:00:00 2001
From: GRAB Racem
Date: Sat, 10 Oct 2026 02:06:15 +0200
Subject: [PATCH 03/74] ci: re-run the suite after the main-branch shard flake
The failed shard is the JavaScriptSemanticGraphBuilder short-circuit
mutation test, which also failed on main run 38003459598 at the same
base commit (08ec2bf8) before this branch started CI, and passed on
main run 38003459600 minutes later at the same commit.
From e17290cd2a17931d7a2af70f74b76b94276bd788 Mon Sep 17 00:00:00 2001
From: GRAB Racem
Date: Sat, 10 Oct 2026 02:34:52 +0200
Subject: [PATCH 04/74] ci: re-run the suite on the corrected main checkpoint
b8a8190b restored the guarded-mutation uncertainty the failing test
encodes, so the shard should pass against the new merge base.
From e4a8e0b728154fe0bf181684ccd0d73df45cf4bf Mon Sep 17 00:00:00 2001
From: Claude
Date: Sat, 10 Oct 2026 00:44:32 +0000
Subject: [PATCH 05/74] fix(javascript): recognize Electron APIs through
renamed bindings
Electron recognition matched callee spellings, so a bundler rename such as
esbuild's `import { ipcMain as ipcMain2 } from "electron"`, or a destructured
`const { contextBridge: bridge } = require("electron")`, hid every IPC
handler, BrowserWindow, contextBridge and utility process behind it.
Resolve each call and construction root lexically to its Electron import or
require origin, and classify the callee under that export's name. Shadowed
locals keep their own meaning.
On Hermes Desktop v0.21.6, whose main process is one esbuild ESM bundle,
recognized main IPC handlers rise from 1 to 204, BrowserWindow constructions
from 0 to 11, and paired renderer transmissions from 1 to 201.
Co-Authored-By: Claude Opus 5.5 (1M context)
Claude-Session: https://claude.ai/code/session_01XPY2GmufMNGQKoEgRUx1pd
---
.../electronStaticAnalysisBrowser.ts | 8 +-
.../javascript/electronStaticAnalysisIpc.ts | 3 +-
.../electronStaticAnalysisValues.ts | 21 ++++-
.../javascript/javascriptSemanticAnalysis.ts | 79 +++++++++++++++++++
.../javascript/javascriptStaticAnalysis.ts | 27 +++----
.../javascriptStaticAnalysisState.ts | 2 +
.../electronApplicationAnalysis.test.ts | 66 ++++++++++++++++
7 files changed, 186 insertions(+), 20 deletions(-)
diff --git a/src/domain/javascript/electronStaticAnalysisBrowser.ts b/src/domain/javascript/electronStaticAnalysisBrowser.ts
index f29e7cf8f4..a44baf89a7 100644
--- a/src/domain/javascript/electronStaticAnalysisBrowser.ts
+++ b/src/domain/javascript/electronStaticAnalysisBrowser.ts
@@ -12,6 +12,7 @@ import type {
import {
boundedExpression,
collectContextBridgeMembers,
+ electronCalleeName,
electronStaticValue,
objectProperty,
} from "./electronStaticAnalysisValues.js";
@@ -19,7 +20,6 @@ import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { addLocatedFinding } from "./javascriptStaticAnalysisFindings.js";
import {
argumentNode,
- calleeName,
range,
staticPath,
staticPathResolutionContext,
@@ -42,7 +42,7 @@ const inspectBrowserWindow = (
node: t.NewExpression,
context: JavaScriptFindingContext,
): void => {
- const name = calleeName(node.callee);
+ const name = electronCalleeName(node.callee, context);
if (name !== "BrowserWindow" && !name.endsWith(".BrowserWindow")) return;
const options = argumentNode(node.arguments[0]);
const collected = collectWindowOptions(context.source, options);
@@ -165,7 +165,7 @@ const inspectContextBridge = (
node: t.CallExpression,
context: JavaScriptFindingContext,
): void => {
- const name = calleeName(node.callee);
+ const name = electronCalleeName(node.callee, context);
const main =
name === "contextBridge.exposeInMainWorld" ||
name.endsWith(".contextBridge.exposeInMainWorld");
@@ -213,7 +213,7 @@ const inspectUtilityProcess = (
node: t.CallExpression,
context: JavaScriptFindingContext,
): void => {
- const name = calleeName(node.callee);
+ const name = electronCalleeName(node.callee, context);
if (name !== "utilityProcess.fork" && !name.endsWith(".utilityProcess.fork"))
return;
const moduleNode = argumentNode(node.arguments[0]);
diff --git a/src/domain/javascript/electronStaticAnalysisIpc.ts b/src/domain/javascript/electronStaticAnalysisIpc.ts
index 9efc2670b8..a560ea1844 100644
--- a/src/domain/javascript/electronStaticAnalysisIpc.ts
+++ b/src/domain/javascript/electronStaticAnalysisIpc.ts
@@ -7,6 +7,7 @@ import type {
} from "./electronStaticAnalysisTypes.js";
import {
boundedExpression,
+ electronCalleeName,
electronStaticValue,
handlerKind,
} from "./electronStaticAnalysisValues.js";
@@ -36,7 +37,7 @@ const inspectIpcCall = (
node: t.CallExpression,
context: JavaScriptFindingContext,
): void => {
- const name = calleeName(node.callee);
+ const name = electronCalleeName(node.callee, context);
const descriptor = ipcDescriptor(name);
if (descriptor === undefined) return;
const channelNode = argumentNode(node.arguments[0]);
diff --git a/src/domain/javascript/electronStaticAnalysisValues.ts b/src/domain/javascript/electronStaticAnalysisValues.ts
index 2a800e6bbd..c2bb6e76a2 100644
--- a/src/domain/javascript/electronStaticAnalysisValues.ts
+++ b/src/domain/javascript/electronStaticAnalysisValues.ts
@@ -6,7 +6,26 @@ import {
readExactJavaScriptLiteral,
semanticStaticPropertyName,
} from "./javascriptAstValues.js";
-import { sourceSlice } from "./javascriptStaticAnalysisHelpers.js";
+import { calleeName, sourceSlice } from "./javascriptStaticAnalysisHelpers.js";
+import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js";
+
+/** Name a callee, spelling a root renamed from Electron as its export. */
+export const electronCalleeName = (
+ callee: t.Node,
+ context: JavaScriptFindingContext,
+): string => {
+ const name = calleeName(callee);
+ let root = callee;
+ while (t.isMemberExpression(root) || t.isOptionalMemberExpression(root)) {
+ if (!t.isNode(root.object)) return name;
+ root = root.object;
+ }
+ if (!t.isIdentifier(root)) return name;
+ const exported = context.electronBindings?.get(root.start ?? -1);
+ return exported === undefined
+ ? name
+ : `${exported}${name.slice(root.name.length)}`;
+};
/** Preserve one literal value or the exact inert expression. */
export const electronStaticValue = (
diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts
index 5a873429e1..b5c1c7f5df 100644
--- a/src/domain/javascript/javascriptSemanticAnalysis.ts
+++ b/src/domain/javascript/javascriptSemanticAnalysis.ts
@@ -29,6 +29,7 @@ import {
semanticVariableScope,
} from "./javascriptSemanticState.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
+import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
import { collectSemanticMemberMutations } from "./javascriptSemanticMemberMutations.js";
import {
collectSemanticReturns,
@@ -282,6 +283,84 @@ const resolveGlobalAliasFact = (
return openGlobalFact(name);
};
+const ELECTRON_MODULE = /^electron(?:\/(?:common|main|renderer|utility))?$/u;
+
+/**
+ * Electron export paths for call and construction roots bound to Electron
+ * under another name, such as esbuild's `import { ipcMain as ipcMain2 }`.
+ * Keys are root identifier offsets; roots spelled as their export are omitted.
+ */
+export const classifyParsedJavaScriptElectronBindings = (
+ file: ParsedJavaScriptSource,
+): ReadonlyMap => {
+ const facts = new Map();
+ const state = createState(file.program);
+ collectDefinitions(file.program, state);
+ const exportsByBinding = new Map();
+ traverseJavaScriptAst(file.program, {
+ enter: (node) => {
+ if (
+ !t.isCallExpression(node) &&
+ !t.isOptionalCallExpression(node) &&
+ !t.isNewExpression(node)
+ )
+ return;
+ const root = calleeRoot(node.callee);
+ if (root === undefined) return;
+ const binding = resolveSemanticBindingState(state, root, root.name);
+ if (binding === undefined) return;
+ let exported = exportsByBinding.get(binding.bindingId);
+ if (exported === undefined) {
+ exported = electronExport(binding, state);
+ exportsByBinding.set(binding.bindingId, exported);
+ }
+ if (exported !== null && exported !== root.name)
+ facts.set(root.start ?? -1, exported);
+ },
+ });
+ return facts;
+};
+
+const calleeRoot = (node: t.Node): t.Identifier | undefined => {
+ while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
+ if (!t.isNode(node.object)) return undefined;
+ node = node.object;
+ }
+ return t.isIdentifier(node) ? node : undefined;
+};
+
+// Read import and require origins directly; general value evaluation is
+// unnecessary here and costly across large vendor bundles.
+const electronExport = (
+ binding: JavaScriptSemanticBindingState,
+ state: JavaScriptSemanticAnalysisState,
+): string | null => {
+ const origin = bindingOrigin(binding, state);
+ return origin !== undefined &&
+ ELECTRON_MODULE.test(origin.specifier) &&
+ origin.importedPath.length > 0
+ ? origin.importedPath.join(".")
+ : null;
+};
+
+const bindingOrigin = (
+ binding: JavaScriptSemanticBindingState,
+ state: JavaScriptSemanticAnalysisState,
+): JavaScriptModuleOrigin | undefined => {
+ if (binding.directOrigins.length > 0)
+ return binding.directOrigins.length === 1
+ ? binding.directOrigins[0]
+ : undefined;
+ const [initializer] = binding.initializers;
+ if (binding.mutable || binding.initializers.length !== 1) return undefined;
+ const origin = semanticRequireOrigin(initializer?.node, state);
+ const projection = initializer?.projection ?? [];
+ return origin === undefined ||
+ !projection.every((key): key is string => typeof key === "string")
+ ? undefined
+ : { ...origin, importedPath: [...origin.importedPath, ...projection] };
+};
+
const createState = (program: t.Program): JavaScriptSemanticAnalysisState => {
const root: JavaScriptSemanticScopeState = {
scopeId: semanticScopeId("program", program),
diff --git a/src/domain/javascript/javascriptStaticAnalysis.ts b/src/domain/javascript/javascriptStaticAnalysis.ts
index af20eaa926..524af811eb 100644
--- a/src/domain/javascript/javascriptStaticAnalysis.ts
+++ b/src/domain/javascript/javascriptStaticAnalysis.ts
@@ -25,9 +25,11 @@ import { finalizeLocatedFindings } from "./javascriptStaticAnalysisFindings.js";
import {
createJavaScriptAnalysisAccumulator,
type JavaScriptAnalysisAccumulator as AnalysisAccumulator,
+ type JavaScriptFindingContext,
} from "./javascriptStaticAnalysisState.js";
import type { JavaScriptStaticAnalysis } from "./javascriptStaticAnalysisTypes.js";
import {
+ classifyParsedJavaScriptElectronBindings,
classifyParsedJavaScriptOpenReceivers,
type JavaScriptOpenReceiverFact,
} from "./javascriptSemanticAnalysis.js";
@@ -57,21 +59,24 @@ export const analyzeParsedJavaScriptStaticSource = (
openReceiverFacts?: ReadonlyMap,
): JavaScriptStaticAnalysis => {
const accumulator = createJavaScriptAnalysisAccumulator(source.length);
- traverseStaticSource(source, file, accumulator, openReceiverFacts);
+ traverseStaticSource(file, {
+ source,
+ accumulator,
+ ...(openReceiverFacts === undefined ? {} : { openReceiverFacts }),
+ electronBindings: classifyParsedJavaScriptElectronBindings(file),
+ });
addSourceMapDirectives(source, file.comments ?? [], accumulator);
return finalizeStaticAnalysis(source, file, accumulator);
};
const traverseStaticSource = (
- source: string,
file: ParsedJavaScriptSource,
- accumulator: AnalysisAccumulator,
- openReceiverFacts?: ReadonlyMap,
+ findings: JavaScriptFindingContext,
): void => {
traverseJavaScriptAst(file, {
enter: (node) => {
- accumulator.visitedNodes += 1;
- inspectNode(source, node, accumulator, openReceiverFacts);
+ findings.accumulator.visitedNodes += 1;
+ inspectNode(node, findings);
return undefined;
},
});
@@ -156,16 +161,10 @@ const finalizeStaticAnalysis = (
};
const inspectNode = (
- source: string,
node: t.Node,
- accumulator: AnalysisAccumulator,
- openReceiverFacts?: ReadonlyMap,
+ findings: JavaScriptFindingContext,
): void => {
- const findings = {
- source,
- accumulator,
- ...(openReceiverFacts === undefined ? {} : { openReceiverFacts }),
- };
+ const { source, accumulator } = findings;
inspectElectronStaticNode(node, findings);
if (t.isCallExpression(node)) {
inspectBundlerRegistration(source, node, accumulator);
diff --git a/src/domain/javascript/javascriptStaticAnalysisState.ts b/src/domain/javascript/javascriptStaticAnalysisState.ts
index 23de6952bf..47e25bccdb 100644
--- a/src/domain/javascript/javascriptStaticAnalysisState.ts
+++ b/src/domain/javascript/javascriptStaticAnalysisState.ts
@@ -65,6 +65,8 @@ export interface JavaScriptFindingContext {
| "local-indexed-db"
| "local-cache-storage"
>;
+ /** Electron export paths for renamed call roots, by identifier offset. */
+ readonly electronBindings?: ReadonlyMap;
}
/** Candidate import, require, worker, or service-worker reference. */
diff --git a/tests/composition/javascript-applications/electronApplicationAnalysis.test.ts b/tests/composition/javascript-applications/electronApplicationAnalysis.test.ts
index 3484f98a24..4180e1818d 100644
--- a/tests/composition/javascript-applications/electronApplicationAnalysis.test.ts
+++ b/tests/composition/javascript-applications/electronApplicationAnalysis.test.ts
@@ -89,6 +89,33 @@ describe("static Electron application analysis", () => {
expect(requestedMembers).toEqual(["*"]);
});
+ it("maps Electron boundaries through bundler-renamed bindings", async () => {
+ const root = await renamedBindingFixtureDirectory();
+
+ const result = await reconstructJavaScriptArtifact({ input_path: root });
+ const graph = parseJavaScriptApplicationGraph(result.graph);
+ const handledChannels = graph.nodes.flatMap(({ kind, observations }) =>
+ kind === "ipc-handler"
+ ? observations.flatMap(({ properties }) =>
+ properties.side === "main" ? [properties.channel] : [],
+ )
+ : [],
+ );
+
+ expect(result.electron_summary).toMatchObject({
+ browser_windows: 1,
+ context_bridge_apis: 1,
+ exposed_api_members: 2,
+ ipc: {
+ main_handlers: 2,
+ paired_renderer_transmissions: 2,
+ unpaired_literal_renderer_transmissions: 0,
+ },
+ utility_processes: 1,
+ });
+ expect(handledChannels.sort()).toEqual(["rea:read", "rea:write"]);
+ });
+
it("returns a tagged cancellation without executing application code", async () => {
const root = await fixtureDirectory();
const controller = new AbortController();
@@ -299,6 +326,45 @@ const fixtureDirectory = async (): Promise => {
return root;
};
+const renamedBindingFixtureDirectory = async (): Promise => {
+ const root = await createTestTempDirectory("rea-electron-renamed-");
+ await Promise.all([
+ writeFile(
+ join(root, "package.json"),
+ JSON.stringify({ name: "rea-electron-renamed", main: "main.mjs" }),
+ ),
+ // Bundlers rename colliding imports, e.g. esbuild's ipcMain2.
+ writeFile(
+ join(root, "main.mjs"),
+ String.raw`
+import { BrowserWindow as BrowserWindow2, ipcMain as ipcMain2 } from "electron";
+import { ipcMain as ipcMain3, utilityProcess as utility } from "electron/main";
+
+new BrowserWindow2({ webPreferences: { preload: "./preload.cjs" } });
+ipcMain2.handle("rea:read", async () => "value");
+ipcMain3.on("rea:write", () => undefined);
+utility.fork("./worker.js");
+function register(ipcMain2) {
+ ipcMain2.handle("rea:shadowed", async () => "local");
+}
+export { register };
+`,
+ ),
+ writeFile(
+ join(root, "preload.cjs"),
+ String.raw`
+const { contextBridge: bridge, ipcRenderer: ipc } = require("electron");
+bridge.exposeInMainWorld("reaApi", {
+ read: () => ipc.invoke("rea:read"),
+ write: (value) => ipc.send("rea:write", value),
+});
+`,
+ ),
+ writeFile(join(root, "worker.js"), "module.exports = {};"),
+ ]);
+ return root;
+};
+
const computedMemberFixtureDirectory = async (): Promise => {
const root = await createTestTempDirectory("rea-electron-computed-members-");
await mkdir(join(root, "native"));
From 7623e21ff02cf4eb00ccb4fa357936e4ed01425e Mon Sep 17 00:00:00 2001
From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Date: Fri, 9 Oct 2026 20:00:11 -0700
Subject: [PATCH 06/74] perf(native): record pwntools diagnostics once in their
Evidence (#1404)
inspect_binary_layout and inspect_recorded_crash passed the decoded report
as both result and rawResult, so every Evidence record carried the whole
report twice (and an MCP response four times, counting its text copy).
The decoded report is the only representation, so raw_result is now null,
as for other single-representation operations.
The real pwntools verifiers asserted the duplicate; they now assert null.
Co-authored-by: Claude Opus 5.5
---
scripts/verify-binary-layout.mjs | 2 +-
scripts/verify/recorded-crash/public.mjs | 2 +-
.../binaryDiagnostics/BinaryLayoutService.test.ts | 14 ++++++++++++++
.../binaryDiagnostics/BinaryLayoutService.ts | 3 ++-
.../binaryDiagnostics/RecordedCrashService.test.ts | 1 +
.../binaryDiagnostics/RecordedCrashService.ts | 3 ++-
tests/fixtures/analysisView.ts | 2 +-
7 files changed, 22 insertions(+), 5 deletions(-)
diff --git a/scripts/verify-binary-layout.mjs b/scripts/verify-binary-layout.mjs
index 996eb39666..1f3b5e4bc4 100644
--- a/scripts/verify-binary-layout.mjs
+++ b/scripts/verify-binary-layout.mjs
@@ -1283,7 +1283,7 @@ async function inspect(
evidence.provider.version,
"pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2",
);
- assert.deepEqual(evidence.normalized_result, evidence.raw_result);
+ assert.equal(evidence.raw_result, null);
assert.equal(evidence.normalized_result.diagnostics.truncated, false);
return evidence.normalized_result;
}
diff --git a/scripts/verify/recorded-crash/public.mjs b/scripts/verify/recorded-crash/public.mjs
index 76713bf9f4..ecdfd14cd2 100644
--- a/scripts/verify/recorded-crash/public.mjs
+++ b/scripts/verify/recorded-crash/public.mjs
@@ -80,7 +80,7 @@ export async function connectRecordedCrash({ entrypoint, environment }) {
evidence.confidence,
debuggerContext ? "derived" : "observed",
);
- assert.deepEqual(evidence.raw_result, report);
+ assert.equal(evidence.raw_result, null);
assert.equal(evidence.subject.local_path, path);
assert.equal(report.artifact.path, path);
assert.equal(
diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts
index e8f3e2c6dd..e65da65f67 100644
--- a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts
+++ b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts
@@ -5,6 +5,7 @@ import {
BINARY_LAYOUT_TEST_PROVIDER,
} from "../../../tests/fixtures/binaryDiagnostics/layout.js";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
+import { parseEvidence } from "../../domain/evidence.js";
import { err, ok } from "../../domain/result.js";
it.each([
@@ -28,6 +29,19 @@ it.each([
},
);
+it("records the decoded layout once in its Evidence", async () => {
+ const layout = binaryLayoutFixture();
+ const service = new BinaryLayoutService({
+ identity: BINARY_LAYOUT_TEST_PROVIDER,
+ inspect: () => Promise.resolve(ok(layout)),
+ });
+ const result = await service.inspect({ path: layout.artifact.path });
+ if (!result.ok) throw result.error;
+ const evidence = parseEvidence(result.value);
+ expect(evidence.normalized_result).toEqual(layout);
+ expect(evidence.raw_result).toBeNull();
+});
+
it("preserves a provider's actionable unsupported reason", async () => {
const failure = new AnalysisCapabilityUnavailableError(
"fixture-layout",
diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.ts b/src/application/binaryDiagnostics/BinaryLayoutService.ts
index 11bfeb76e9..bdb07da56a 100644
--- a/src/application/binaryDiagnostics/BinaryLayoutService.ts
+++ b/src/application/binaryDiagnostics/BinaryLayoutService.ts
@@ -78,7 +78,8 @@ export class BinaryLayoutService {
operation: OPERATION,
parameters: input.data,
result: value,
- rawResult: value,
+ // The decoded report is the only representation; do not repeat it.
+ rawResult: null,
confidence: "observed",
limitations: value.limitations,
locations: [{ kind: "artifact-path", path: value.artifact.path }],
diff --git a/src/application/binaryDiagnostics/RecordedCrashService.test.ts b/src/application/binaryDiagnostics/RecordedCrashService.test.ts
index 6060aef332..0ab49ee1ba 100644
--- a/src/application/binaryDiagnostics/RecordedCrashService.test.ts
+++ b/src/application/binaryDiagnostics/RecordedCrashService.test.ts
@@ -24,6 +24,7 @@ it("labels an envelope containing debugger-derived maps as derived", async () =>
const evidence = parseEvidence(result.value);
expect(evidence.confidence).toBe("derived");
expect(evidence.normalized_result).toEqual(fixture);
+ expect(evidence.raw_result).toBeNull();
});
it.each([
diff --git a/src/application/binaryDiagnostics/RecordedCrashService.ts b/src/application/binaryDiagnostics/RecordedCrashService.ts
index cfc9278dc1..5a4fff645d 100644
--- a/src/application/binaryDiagnostics/RecordedCrashService.ts
+++ b/src/application/binaryDiagnostics/RecordedCrashService.ts
@@ -83,7 +83,8 @@ export class RecordedCrashService {
operation: OPERATION,
parameters: input.data,
result: value,
- rawResult: value,
+ // The decoded report is the only representation; do not repeat it.
+ rawResult: null,
confidence:
value.debugger.status === "available" ? "derived" : "observed",
limitations: value.limitations,
diff --git a/tests/fixtures/analysisView.ts b/tests/fixtures/analysisView.ts
index 0f928a22a2..d3be2b1e3b 100644
--- a/tests/fixtures/analysisView.ts
+++ b/tests/fixtures/analysisView.ts
@@ -99,7 +99,7 @@ export const analysisViewLayoutEvidence = (
operation: "inspect_binary_layout",
parameters: { path: layout.artifact.path },
result: layout,
- rawResult: layout,
+ rawResult: null,
confidence: "observed",
limitations: layout.limitations,
locations: [{ kind: "artifact-path", path: layout.artifact.path }],
From 1f0f728c63a4d9f32fc183c6a41627eedb8d1fe2 Mon Sep 17 00:00:00 2001
From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Date: Fri, 9 Oct 2026 20:00:35 -0700
Subject: [PATCH 07/74] perf(hopper): stop repeating Hopper replies as
raw_result (#1405)
HopperProvider passed the bridge reply as rawResult and, for every
operation except address_to_file_offset, the same object as the result.
Every Hopper Evidence record (decompilation, procedure and string
listings, xrefs, ...) therefore carried its complete result twice, and
an MCP response four times counting its text copy.
raw_result is now null unless REA transformed the reply, which only the
file-offset mapping does; that case keeps Hopper's original reply.
Co-authored-by: Claude Opus 5.5
---
src/hopper/HopperProvider.ts | 3 ++-
tests/acceptance/applications/runtime.test.ts | 2 ++
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/hopper/HopperProvider.ts b/src/hopper/HopperProvider.ts
index 5b1da0ba9b..6c314ddb68 100644
--- a/src/hopper/HopperProvider.ts
+++ b/src/hopper/HopperProvider.ts
@@ -249,7 +249,8 @@ export class HopperProvider implements AnalysisProviderCandidate {
? {
ok: true,
value: createAnalysisExecution(mapped.value, executionProvider, {
- rawResult: result.value,
+ // Only the file-offset mapping differs from Hopper's reply.
+ rawResult: mapped === result ? null : result.value,
...(profile === undefined ? {} : { analysisProfile: profile }),
limitations: [
...(preparedImage === undefined
diff --git a/tests/acceptance/applications/runtime.test.ts b/tests/acceptance/applications/runtime.test.ts
index 1a50c3fc10..bfa75b7219 100644
--- a/tests/acceptance/applications/runtime.test.ts
+++ b/tests/acceptance/applications/runtime.test.ts
@@ -144,6 +144,8 @@ describe("production stdio runtime", () => {
expect(evidence.operation).toBe("current_document");
expect(evidence.provider.id).toBe("hopper");
expect(evidence.normalized_result).toBe(observation.normalized_result);
+ // Hopper's reply is the result; it is not repeated as raw_result.
+ expect(evidence.raw_result).toBeNull();
expect(records).toContainEqual(
expect.objectContaining({
application: "rea",
From 8550913cda311e5340cf012b9c4afd622e36e35b Mon Sep 17 00:00:00 2001
From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com>
Date: Sat, 10 Oct 2026 11:00:53 +0800
Subject: [PATCH 08/74] fix(javascript)!: stop copying literals into semantic
metadata (#1407)
Commit literal values with canonical JSON SHA-256 in semantic role keys
and use a type label for strings. Preserve complete values, provenance and
literal queries while avoiding two payload copies in each string node.
Verify real compiled CLI/default stdio MCP, typed union seeds, empty
strings and trace continuation with a large source-owned fixture.
Refs #723
BREAKING CHANGE: Newly produced semantic literal node IDs use
value-sha256 role keys, and string labels read "string literal". Read exact
values from properties.value and obtain node IDs from the selected parent
graph. Previously stored Evidence and its original IDs remain valid.
---
docs/javascript-application-workflows.md | 12 ++
.../JavaScriptSemanticGraphValueProjection.ts | 7 +-
.../mcp/javascriptLiteralPayload.test.ts | 154 ++++++++++++++++++
3 files changed, 171 insertions(+), 2 deletions(-)
create mode 100644 tests/boundary/mcp/javascriptLiteralPayload.test.ts
diff --git a/docs/javascript-application-workflows.md b/docs/javascript-application-workflows.md
index cb3e1e5e9c..c257464164 100644
--- a/docs/javascript-application-workflows.md
+++ b/docs/javascript-application-workflows.md
@@ -90,6 +90,18 @@ keep the result ambiguous. Runtime Evidence can later corroborate an exact
mapped candidate, but structural reachability, semantic influence, runtime
observation, and causal proof remain separate claims.
+Semantic literal nodes keep their complete value in `properties.value`; literal
+queries match that field. String labels read `string literal` rather than
+repeating the payload. A literal's `identity.role_key` commits its canonical
+JSON value as `value-sha256:`, together with its binding and property
+role. This keeps large values out of identity and display metadata without
+truncating their Evidence.
+
+New analyses therefore produce different literal node IDs from the earlier
+payload-bearing role keys. Use IDs returned by the selected parent graph and
+read literal values from `properties.value`, rather than decoding role keys or
+labels. Previously stored graphs and their original IDs remain valid inputs.
+
## Version comparison
REA pairs entities only when a tier produces one unique candidate on each side.
diff --git a/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts b/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts
index b8002fcd0c..dfe8267b6f 100644
--- a/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts
+++ b/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts
@@ -2,6 +2,7 @@ import {
semanticContainer,
semanticPropertyPointer,
} from "../../domain/javascript/javascriptSemanticSlots.js";
+import { digestCanonicalValue } from "../../domain/canonicalDigest.js";
import type { JavaScriptSemanticBinding } from "../../domain/javascript/javascriptSemanticIr.js";
import type {
JavaScriptSemanticProperty,
@@ -156,9 +157,11 @@ const addLiteralNode = (
) =>
retainSemanticGraphNode(context.state, context.file, {
kind: "literal",
- roleKey: `literal:${binding.bindingId}:${role}:${JSON.stringify(value)}`,
+ // Identity needs an exact value commitment, not another payload copy.
+ // Keep the complete literal in properties for queries and Evidence export.
+ roleKey: `literal:${binding.bindingId}:${role}:value-sha256:${digestCanonicalValue(value, "Semantic literal")}`,
location: binding.definitions[0]?.location ?? null,
- label: JSON.stringify(value),
+ label: typeof value === "string" ? "string literal" : JSON.stringify(value),
functionNodeId:
context.bindingNodes.get(binding.bindingId)?.function_node_id ?? null,
properties: { value },
diff --git a/tests/boundary/mcp/javascriptLiteralPayload.test.ts b/tests/boundary/mcp/javascriptLiteralPayload.test.ts
new file mode 100644
index 0000000000..b7fcbb8764
--- /dev/null
+++ b/tests/boundary/mcp/javascriptLiteralPayload.test.ts
@@ -0,0 +1,154 @@
+import { Client } from "@modelcontextprotocol/client";
+import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
+import { createHash } from "node:crypto";
+import { writeFile } from "node:fs/promises";
+import { join, resolve } from "node:path";
+import { expect } from "vitest";
+
+import { toolContract } from "../../../src/contracts/toolContracts.js";
+import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
+import { javaScriptSemanticTraceResultSchema } from "../../../src/domain/javascript/javascriptSemanticTraceSchemas.js";
+import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
+import { cliTest } from "../../support/cli/cliFixture.js";
+
+cliTest(
+ "delivers complete large literals through CLI and default stdio MCP without metadata copies",
+ async ({ cli }) => {
+ const root = await createTestTempDirectory("rea-literal-payload-");
+ const value = 'rea_literal_payload_ğ\\"\0'.padEnd(24, "x").repeat(65_536);
+ const source = [
+ `export const payload = ${JSON.stringify(value)};`,
+ 'export const choice = flag ? 1 : "1";',
+ 'export const empty = "";',
+ ].join("\n");
+ await writeFile(join(root, "main.js"), source);
+ const cliResponse = await cli.run({
+ arguments: [
+ "analyze-javascript-application",
+ root,
+ "--artifact-format",
+ "directory",
+ "--json",
+ ],
+ timeoutMs: 60_000,
+ });
+ expect(cliResponse.exitCode).toBe(0);
+ const cliEvidence = toolContract(
+ "analyze_javascript_application",
+ ).outputSchema.parse(cliResponse.json);
+
+ const client = new Client({ name: "literal-payload-e2e", version: "1" });
+ const transport = new StdioClientTransport({
+ command: process.execPath,
+ args: [resolve("scripts/rea.mjs"), "mcp"],
+ cwd: process.cwd(),
+ env: { PATH: process.env.PATH ?? "" },
+ stderr: "pipe",
+ });
+ transport.stderr?.on("data", () => undefined);
+ try {
+ await client.connect(transport);
+ const response = await client.callTool({
+ name: "analyze_javascript_application",
+ arguments: { input_path: root, format: "directory" },
+ });
+ // The old payload-bearing role key/label exceed the default wire budget.
+ expect(response.isError).not.toBe(true);
+ const evidence = toolContract(
+ "analyze_javascript_application",
+ ).outputSchema.parse(response.structuredContent);
+ expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
+ const text = response.content.find((item) => item.type === "text");
+ if (text?.type !== "text") throw new Error("Missing MCP text result");
+ const textEvidence = toolContract(
+ "analyze_javascript_application",
+ ).outputSchema.parse(JSON.parse(text.text));
+ expect(textEvidence.evidence_id).toBe(evidence.evidence_id);
+ const graph = javascriptApplicationAnalysisResultSchema.parse(
+ evidence.normalized_result,
+ ).semantic_graph;
+ const literal = graph.nodes.find(
+ (node) => node.kind === "literal" && node.properties.value === value,
+ );
+ if (literal === undefined) throw new Error("Complete literal was lost");
+ const sourceSha256 = createHash("sha256").update(source).digest("hex");
+ expect(literal.identity).toMatchObject({
+ module_path: "main.js",
+ artifact_sha256: sourceSha256,
+ });
+ expect(literal.evidence.location).toMatchObject({
+ available: true,
+ value: { kind: "source-range", source: "main.js" },
+ });
+ const choices = graph.nodes.filter(
+ (node) =>
+ node.kind === "literal" &&
+ (node.properties.value === 1 || node.properties.value === "1"),
+ );
+ expect(choices.length).toBe(2);
+ expect(new Set(choices.map((node) => node.node_id)).size).toBe(2);
+ expect(
+ graph.nodes.some(
+ (node) => node.kind === "literal" && node.properties.value === "",
+ ),
+ ).toBe(true);
+
+ const traceResponse = await client.callTool({
+ name: "trace_javascript_semantics",
+ arguments: {
+ application: {
+ kind: "retained-evidence",
+ evidence_id: evidence.evidence_id,
+ },
+ query: {
+ seed: { kind: "semantic-node", node_id: literal.node_id },
+ direction: "backward-provenance",
+ },
+ },
+ });
+ expect(traceResponse.isError).not.toBe(true);
+ const traceEvidence = toolContract(
+ "trace_javascript_semantics",
+ ).outputSchema.parse(traceResponse.structuredContent);
+ const trace = javaScriptSemanticTraceResultSchema.parse(
+ traceEvidence.normalized_result,
+ );
+ expect(
+ trace.nodes.some(
+ (node) =>
+ node.node_id === literal.node_id && node.properties.value === value,
+ ),
+ ).toBe(true);
+ const valueTraceResponse = await client.callTool({
+ name: "trace_javascript_semantics",
+ arguments: {
+ application: {
+ kind: "retained-evidence",
+ evidence_id: evidence.evidence_id,
+ },
+ query: {
+ seed: { kind: "literal", value: "1" },
+ direction: "backward-provenance",
+ },
+ },
+ });
+ expect(valueTraceResponse.isError).not.toBe(true);
+ const valueTraceEvidence = toolContract(
+ "trace_javascript_semantics",
+ ).outputSchema.parse(valueTraceResponse.structuredContent);
+ const valueTrace = javaScriptSemanticTraceResultSchema.parse(
+ valueTraceEvidence.normalized_result,
+ );
+ expect(valueTrace.seed_node_ids).toEqual(
+ choices
+ .filter((node) => node.properties.value === "1")
+ .map((node) => node.node_id),
+ );
+ await client.ping();
+ } finally {
+ await client.close();
+ await transport.close();
+ }
+ },
+ 60_000,
+);
From de8fc6719596305b1d76204e6b052cf46d7e7fc3 Mon Sep 17 00:00:00 2001
From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com>
Date: Sat, 10 Oct 2026 11:23:47 +0800
Subject: [PATCH 09/74] fix(javascript): retain shorter literal metadata
encodings (#1408)
Choose the shorter complete JSON or digest key and string display label.
Short values keep their existing identities and labels, while large values
retain the payload-copy reduction without an arbitrary truncation limit.
---
docs/javascript-application-workflows.md | 18 ++++++++++--------
.../JavaScriptSemanticGraphValueProjection.ts | 17 +++++++++++++----
2 files changed, 23 insertions(+), 12 deletions(-)
diff --git a/docs/javascript-application-workflows.md b/docs/javascript-application-workflows.md
index c257464164..0093710351 100644
--- a/docs/javascript-application-workflows.md
+++ b/docs/javascript-application-workflows.md
@@ -91,16 +91,18 @@ mapped candidate, but structural reachability, semantic influence, runtime
observation, and causal proof remain separate claims.
Semantic literal nodes keep their complete value in `properties.value`; literal
-queries match that field. String labels read `string literal` rather than
-repeating the payload. A literal's `identity.role_key` commits its canonical
-JSON value as `value-sha256:`, together with its binding and property
-role. This keeps large values out of identity and display metadata without
+queries match that field. String labels use `string literal` when that is
+shorter than the complete JSON value. A literal's `identity.role_key` commits
+its canonical JSON value as `value-sha256:` when the digest key is
+shorter; smaller values keep their existing JSON key. This keeps large values
+out of identity and display metadata without expanding short values or
truncating their Evidence.
-New analyses therefore produce different literal node IDs from the earlier
-payload-bearing role keys. Use IDs returned by the selected parent graph and
-read literal values from `properties.value`, rather than decoding role keys or
-labels. Previously stored graphs and their original IDs remain valid inputs.
+New analyses therefore produce different IDs for hashed literals from the
+earlier payload-bearing role keys. Use IDs returned by the selected parent
+graph and read literal values from `properties.value`, rather than decoding
+role keys or labels. Previously stored graphs and their original IDs remain
+valid inputs.
## Version comparison
diff --git a/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts b/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts
index dfe8267b6f..febe23d247 100644
--- a/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts
+++ b/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts
@@ -154,18 +154,27 @@ const addLiteralNode = (
binding: JavaScriptSemanticBinding,
value: string | number | boolean | null,
role: string,
-) =>
- retainSemanticGraphNode(context.state, context.file, {
+) => {
+ const encodedValue = JSON.stringify(value);
+ const valueDigest = `value-sha256:${digestCanonicalValue(value, "Semantic literal")}`;
+ const valueKey =
+ encodedValue.length <= valueDigest.length ? encodedValue : valueDigest;
+ const stringLabel = "string literal";
+ return retainSemanticGraphNode(context.state, context.file, {
kind: "literal",
// Identity needs an exact value commitment, not another payload copy.
// Keep the complete literal in properties for queries and Evidence export.
- roleKey: `literal:${binding.bindingId}:${role}:value-sha256:${digestCanonicalValue(value, "Semantic literal")}`,
+ roleKey: `literal:${binding.bindingId}:${role}:${valueKey}`,
location: binding.definitions[0]?.location ?? null,
- label: typeof value === "string" ? "string literal" : JSON.stringify(value),
+ label:
+ typeof value === "string" && encodedValue.length > stringLabel.length
+ ? stringLabel
+ : encodedValue,
functionNodeId:
context.bindingNodes.get(binding.bindingId)?.function_node_id ?? null,
properties: { value },
});
+};
/** Create one canonical slot for a root binding and exact static property path. */
export const semanticPropertySlot = (
From ca3131b36ccf025f64b9e43ecae2698d37747de7 Mon Sep 17 00:00:00 2001
From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Date: Fri, 9 Oct 2026 20:29:48 -0700
Subject: [PATCH 10/74] perf(cli): write compact JSON when output is not a
terminal (#1413)
`--json` / `--format json` always indented its streamed result. Agents and
scripts read REA through pipes and redirects, where indentation carries no
information: on real application results it added 16-45% model tokens
(o200k proxy) and 25-73% bytes. The skill's CLI fallback uses `--json`.
Indent only when stdout is a TTY, as `gh --json` does; piped or redirected
JSON is compact. The JSON value, `jsonl`, `--filter-output`,
`--full-output` and the token-window paths are unchanged. `jq .` restores
indentation for files.
Co-authored-by: Claude Opus 5.5
---
docs/cli.md | 5 +++-
scripts/verify/javascript/output.mjs | 6 ++--
src/cli/jsonOutput.ts | 18 ++++++++++--
.../tools/historicalCaptureResources.test.ts | 28 +++++++++++--------
tests/boundary/cli/jsonOutput.test.ts | 15 ++++++++++
5 files changed, 53 insertions(+), 19 deletions(-)
diff --git a/docs/cli.md b/docs/cli.md
index ed5f810e98..2a9863c1e9 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -179,7 +179,10 @@ user permissions; consult the chosen guide for host requirements and effects.
## Output and exit status
The default terminal format is TOON. Use `--json` when saving results for a
-JSON consumer. Output selection and formatting do not change operation status.
+JSON consumer. `--json` is indented on a terminal and compact when piped or
+redirected, which keeps large results smaller for agents and scripts; pipe
+through `jq .` for indented files. Output selection and formatting do not change
+operation status.
| Status | Meaning |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
diff --git a/scripts/verify/javascript/output.mjs b/scripts/verify/javascript/output.mjs
index 13dfa916c7..dbdfdc3402 100644
--- a/scripts/verify/javascript/output.mjs
+++ b/scripts/verify/javascript/output.mjs
@@ -54,15 +54,15 @@ try {
data: { ...evidence, normalized_result: { payload: [marker] } },
meta: metadata,
};
- const skeleton = JSON.stringify(template, null, format === "json" ? 2 : 0);
+ // The destination is a redirected file, so both formats are compact.
+ const skeleton = JSON.stringify(template);
const encodedMarker = JSON.stringify(marker);
const markerIndex = skeleton.indexOf(encodedMarker);
assert.ok(markerIndex >= 0);
assert.equal(markerIndex, skeleton.lastIndexOf(encodedMarker));
const prefix = skeleton.slice(0, markerIndex);
const suffix = `${skeleton.slice(markerIndex + encodedMarker.length)}\n`;
- const indentation = prefix.slice(prefix.lastIndexOf("\n") + 1);
- const separator = format === "json" ? `,\n${indentation}` : ",";
+ const separator = ",";
const expectedHash = createHash("sha256");
expectedHash.update(prefix);
for (let index = 0; index < count; index += 1) {
diff --git a/src/cli/jsonOutput.ts b/src/cli/jsonOutput.ts
index b42bd82bdf..f7422a7bf2 100644
--- a/src/cli/jsonOutput.ts
+++ b/src/cli/jsonOutput.ts
@@ -4,16 +4,27 @@ import { bufferedJsonParts, jsonParts } from "../domain/jsonSerialization.js";
type JsonOutputFormat = "json" | "jsonl";
-/** Write ordinary JSON without constructing a document-sized string. */
+/**
+ * Write ordinary JSON without constructing a document-sized string.
+ *
+ * `json` is indented for a terminal reader. Piped or redirected output, which
+ * agents and scripts consume, is compact: indentation adds no information and
+ * measured 16-45% more model tokens on real results. The value is identical.
+ */
export const writeJsonOutput = async (
value: unknown,
destination: Writable,
format: JsonOutputFormat = "json",
): Promise => {
- for (const part of bufferedJsonParts(jsonOutputParts(value, format)))
+ for (const part of bufferedJsonParts(
+ jsonOutputParts(value, format, isTerminal(destination)),
+ ))
await writePart(destination, part);
};
+const isTerminal = (destination: Writable): boolean =>
+ "isTTY" in destination && destination.isTTY === true;
+
const writePart = (destination: Writable, part: string): Promise =>
new Promise((resolve, reject) => {
const onError = (cause: unknown): void => reject(cause);
@@ -39,6 +50,7 @@ const writePart = (destination: Writable, part: string): Promise =>
export function* jsonOutputParts(
value: unknown,
format: JsonOutputFormat,
+ pretty = true,
): Generator {
if (format === "jsonl" && Array.isArray(value)) {
for (const item of value) {
@@ -47,6 +59,6 @@ export function* jsonOutputParts(
}
return;
}
- yield* jsonParts(value, format === "json");
+ yield* jsonParts(value, pretty && format === "json");
yield "\n";
}
diff --git a/tests/acceptance/tools/historicalCaptureResources.test.ts b/tests/acceptance/tools/historicalCaptureResources.test.ts
index 7aea0fcce2..87e509e652 100644
--- a/tests/acceptance/tools/historicalCaptureResources.test.ts
+++ b/tests/acceptance/tools/historicalCaptureResources.test.ts
@@ -83,18 +83,22 @@ it("reports real HAR heap exhaustion through CLI and MCP and preserves server us
});
expect(JSON.stringify(response)).not.toContain("caller-marked");
expect(JSON.stringify(response)).not.toContain("rea doctor");
- await expect(
- execute(process.execPath, [
- cli,
- "inspect-web-network-capture",
- path,
- "har",
- "--json",
- ]),
- ).rejects.toMatchObject({
- code: 1,
- stdout: expect.stringContaining('"code": "resource_constraint"'),
- });
+ const failed: unknown = await execute(process.execPath, [
+ cli,
+ "inspect-web-network-capture",
+ path,
+ "har",
+ "--json",
+ ]).then(
+ () => undefined,
+ (cause: unknown) => cause,
+ );
+ expect(failed).toMatchObject({ code: 1 });
+ const stdout =
+ typeof failed === "object" && failed !== null && "stdout" in failed
+ ? String(failed.stdout)
+ : "";
+ expect(JSON.parse(stdout)).toMatchObject({ code: "resource_constraint" });
const small = join(root, "small.har");
await writeFile(small, capture(2));
await client.ping();
diff --git a/tests/boundary/cli/jsonOutput.test.ts b/tests/boundary/cli/jsonOutput.test.ts
index 0294073885..fd03f0e920 100644
--- a/tests/boundary/cli/jsonOutput.test.ts
+++ b/tests/boundary/cli/jsonOutput.test.ts
@@ -16,6 +16,7 @@ import { silentLogger } from "../../../src/logger.js";
const collect = async (
value: unknown,
format: "json" | "jsonl" = "json",
+ terminal = true,
): Promise => {
const chunks: Buffer[] = [];
const destination = new Writable({
@@ -28,6 +29,7 @@ const collect = async (
callback();
},
});
+ if (terminal) Object.defineProperty(destination, "isTTY", { value: true });
await writeJsonOutput(value, destination, format);
return Buffer.concat(chunks).toString("utf8");
};
@@ -52,6 +54,16 @@ describe("incremental CLI JSON", () => {
expect(await collect(value)).toBe(`${JSON.stringify(value, null, 2)}\n`);
});
+ it("indents JSON only for a terminal and writes piped JSON compactly", async () => {
+ const value = { nested: [{ value: null }], text: "雪𝟠" };
+ expect(await collect(value, "json", true)).toBe(
+ `${JSON.stringify(value, null, 2)}\n`,
+ );
+ expect(await collect(value, "json", false)).toBe(
+ `${JSON.stringify(value)}\n`,
+ );
+ });
+
it("writes compact JSONL records and preserves an empty record set", async () => {
const value = [{ text: "first\nrecord" }, { text: "雪𝟠" }];
expect(await collect(value, "jsonl")).toBe(
@@ -78,6 +90,7 @@ describe("incremental CLI JSON", () => {
}, 1);
},
});
+ Object.defineProperty(destination, "isTTY", { value: true });
await writeJsonOutput({ leaf }, destination);
expect(largestChunk).toBeLessThan(Buffer.byteLength(leaf));
const actual = Buffer.concat(chunks).toString("utf8");
@@ -212,6 +225,8 @@ describe("streamed CLI result surface", () => {
expect(operation).toBe("inspect_managed_members");
return value;
});
+ // A terminal destination exercises the indented encoding past the limit.
+ Object.defineProperty(destination, "isTTY", { value: true });
const argv = ["inspect-managed-members", "fixture.dll", "--json"];
const output = createStreamedCliJsonOutput(argv, destination);
if (output === undefined) throw new Error("Missing JSON output surface");
From 4d8ef3f35022f286e5a6b2ee250cfb336e14a4ad Mon Sep 17 00:00:00 2001
From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com>
Date: Sat, 10 Oct 2026 11:30:42 +0800
Subject: [PATCH 11/74] perf(javascript): aggregate semantic family coverage
once (#1410)
Reduce repeated semantic relation and unknown family scans while preserving exact complete Evidence and coverage.
---
.../JavaScriptSemanticGraphBuilder.ts | 26 ++++++++++++-------
1 file changed, 17 insertions(+), 9 deletions(-)
diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts
index a951a7a55e..45ded50518 100644
--- a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts
+++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts
@@ -175,7 +175,21 @@ export const createJavaScriptSemanticGraphProjection =
): Value => {
bindSemanticGraphApplicationNodes(state, applicationGraph);
if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state);
+ const relations = [...state.relations.values()];
const unknowns = [...state.unknowns.values()];
+ type Family = (typeof JAVASCRIPT_SEMANTIC_RELATION_FAMILIES)[number];
+ const retainedByFamily = new Map();
+ const unknownIdsByFamily = new Map();
+ for (const relation of relations) {
+ const family = JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation];
+ retainedByFamily.set(family, (retainedByFamily.get(family) ?? 0) + 1);
+ }
+ for (const unknown of unknowns) {
+ const identifiers = unknownIdsByFamily.get(unknown.family);
+ if (identifiers === undefined)
+ unknownIdsByFamily.set(unknown.family, [unknown.unknown_id]);
+ else identifiers.push(unknown.unknown_id);
+ }
const graph = factory({
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: rootArtifactSha256,
@@ -183,7 +197,7 @@ export const createJavaScriptSemanticGraphProjection =
root_node_ids: [...state.roots],
evidence_contexts: state.evidenceContexts.contexts,
nodes: [...state.nodes.values()],
- relations: [...state.relations.values()],
+ relations,
fingerprints,
unknowns,
coverage: {
@@ -204,15 +218,9 @@ export const createJavaScriptSemanticGraphProjection =
families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({
family,
status: semanticFamilyStatus(family),
- retained_relations: [...state.relations.values()].filter(
- (relation) =>
- JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation] ===
- family,
- ).length,
+ retained_relations: retainedByFamily.get(family) ?? 0,
omitted_relations: truncatedFiles > 0 ? null : 0,
- unknown_ids: unknowns
- .filter((unknown) => unknown.family === family)
- .map(({ unknown_id: identifier }) => identifier),
+ unknown_ids: unknownIdsByFamily.get(family) ?? [],
})),
},
limitations: [
From 81cdafc132c7ba3bd6e12b1169f9e3687025d07b Mon Sep 17 00:00:00 2001
From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com>
Date: Sat, 10 Oct 2026 11:45:42 +0800
Subject: [PATCH 12/74] =?UTF-8?q?docs:=20celebrate=2050,000=20GitHub=20sta?=
=?UTF-8?q?rs=20=F0=9F=8E=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 2 +-
README_ar.md | 2 +-
README_de.md | 2 +-
README_es.md | 2 +-
README_fa.md | 2 +-
README_fr.md | 2 +-
README_ja.md | 2 +-
README_ko.md | 2 +-
README_pl.md | 2 +-
README_pt-BR.md | 2 +-
README_ru.md | 2 +-
README_th.md | 2 +-
README_tr.md | 2 +-
README_uk.md | 2 +-
README_vi.md | 2 +-
README_zh-TW.md | 2 +-
README_zh.md | 2 +-
17 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index 7509d81ea7..e345a56f11 100644
--- a/README.md
+++ b/README.md
@@ -307,7 +307,7 @@ Report vulnerabilities through [SECURITY.md](SECURITY.md).
## Star history
-🎉 **40,000 GitHub stars — thank you!**
+🎉 **50,000 GitHub stars — thank you!**
Thanks to everyone using REA, reporting bugs, requesting features, testing builds, and contributing fixes.
diff --git a/README_ar.md b/README_ar.md
index 92151b9c7e..43bb3adc93 100644
--- a/README_ar.md
+++ b/README_ar.md
@@ -258,7 +258,7 @@ npx rea-agents@latest setup
## سجل النجوم
-🎉 **40,000 نجمة على GitHub — شكرًا لكم!**
+🎉 **50,000 نجمة على GitHub — شكرًا لكم!**
شكرًا لكل من يستخدم REA، ويبلّغ عن الأخطاء، ويقترح ميزات، ويختبر إصدارات البناء، ويساهم بالإصلاحات.
diff --git a/README_de.md b/README_de.md
index b71795bc0c..776aa649d1 100644
--- a/README_de.md
+++ b/README_de.md
@@ -256,7 +256,7 @@ Melde Sicherheitslücken gemäß [SECURITY.md](SECURITY.md).
## Sternverlauf
-🎉 **40.000 GitHub-Sterne – vielen Dank!**
+🎉 **50.000 GitHub-Sterne – vielen Dank!**
Danke an alle, die REA nutzen, Fehler melden, Funktionswünsche äußern, Builds testen und Fehlerbehebungen beitragen.
diff --git a/README_es.md b/README_es.md
index ac86b1f4db..1dafea412d 100644
--- a/README_es.md
+++ b/README_es.md
@@ -256,7 +256,7 @@ Informa de vulnerabilidades siguiendo [SECURITY.md](SECURITY.md).
## Historial de estrellas
-🎉 **40.000 estrellas en GitHub: ¡gracias!**
+🎉 **50.000 estrellas en GitHub: ¡gracias!**
Gracias a quienes usan REA, informan de errores, solicitan funciones, prueban compilaciones y contribuyen con correcciones.
diff --git a/README_fa.md b/README_fa.md
index 7b2b307d97..f7c70ca1d4 100644
--- a/README_fa.md
+++ b/README_fa.md
@@ -241,7 +241,7 @@ npx rea-agents@latest setup
## تاریخچهٔ ستارهها
-🎉 **۴۰٬۰۰۰ ستارهٔ GitHub — سپاسگزاریم!**
+🎉 **۵۰٬۰۰۰ ستارهٔ GitHub — سپاسگزاریم!**
از همهٔ کسانی که از REA استفاده میکنند، خطاها را گزارش میدهند، قابلیتهای جدید پیشنهاد میکنند، buildها را آزمایش میکنند و در بهبود پروژه مشارکت دارند سپاسگزاریم.
diff --git a/README_fr.md b/README_fr.md
index 8fa07803b9..00f22dfd9b 100644
--- a/README_fr.md
+++ b/README_fr.md
@@ -257,7 +257,7 @@ Signalez les vulnérabilités via [SECURITY.md](SECURITY.md).
## Historique des étoiles
-🎉 **40 000 étoiles GitHub — merci !**
+🎉 **50 000 étoiles GitHub — merci !**
Merci à toutes les personnes qui utilisent REA, signalent des bugs, proposent des fonctionnalités, testent les builds et contribuent des correctifs.
diff --git a/README_ja.md b/README_ja.md
index b827c3c040..c100c1f16d 100644
--- a/README_ja.md
+++ b/README_ja.md
@@ -258,7 +258,7 @@ npx rea-agents@latest setup
## スター履歴
-🎉 **GitHub スター 40,000 件、ありがとうございます!**
+🎉 **GitHub スター 50,000 件、ありがとうございます!**
REA の利用、バグ報告、機能の要望、ビルドのテスト、修正への貢献に感謝します。
diff --git a/README_ko.md b/README_ko.md
index 8767920595..9dda2ee1bd 100644
--- a/README_ko.md
+++ b/README_ko.md
@@ -258,7 +258,7 @@ npx rea-agents@latest setup
## 스타 기록
-🎉 **GitHub 스타 40,000개, 감사합니다!**
+🎉 **GitHub 스타 50,000개, 감사합니다!**
REA를 사용하고, 버그를 보고하고, 기능을 요청하고, 빌드를 테스트하고, 수정에 기여해 주신 모든 분께 감사드립니다.
diff --git a/README_pl.md b/README_pl.md
index bdd062da9b..f234fe0341 100644
--- a/README_pl.md
+++ b/README_pl.md
@@ -256,7 +256,7 @@ Zgłaszaj podatności zgodnie z [SECURITY.md](SECURITY.md).
## Historia gwiazdek
-🎉 **40 000 gwiazdek na GitHubie — dziękujemy!**
+🎉 **50 000 gwiazdek na GitHubie — dziękujemy!**
Dziękujemy wszystkim, którzy używają REA, zgłaszają błędy, proponują nowe funkcje, testują kompilacje i przesyłają poprawki.
diff --git a/README_pt-BR.md b/README_pt-BR.md
index 711f7d7881..a788e29858 100644
--- a/README_pt-BR.md
+++ b/README_pt-BR.md
@@ -256,7 +256,7 @@ Relate vulnerabilidades conforme [SECURITY.md](SECURITY.md).
## Histórico de estrelas
-🎉 **40.000 estrelas no GitHub — muito obrigado!**
+🎉 **50.000 estrelas no GitHub — muito obrigado!**
Obrigado a todos que usam o REA, relatam bugs, sugerem funcionalidades, testam builds e contribuem com correções.
diff --git a/README_ru.md b/README_ru.md
index 6b44940e96..cc845a6227 100644
--- a/README_ru.md
+++ b/README_ru.md
@@ -256,7 +256,7 @@ npx rea-agents@latest setup
## История звёзд
-🎉 **40 000 звёзд на GitHub — спасибо!**
+🎉 **50 000 звёзд на GitHub — спасибо!**
Спасибо всем, кто использует REA, сообщает об ошибках, предлагает новые функции, тестирует сборки и помогает с исправлениями.
diff --git a/README_th.md b/README_th.md
index c66e1a3932..2dd8b83e5e 100644
--- a/README_th.md
+++ b/README_th.md
@@ -256,7 +256,7 @@ npx rea-agents@latest setup
## ประวัติดาว
-🎉 **40,000 ดาวบน GitHub — ขอบคุณทุกคน!**
+🎉 **50,000 ดาวบน GitHub — ขอบคุณทุกคน!**
ขอบคุณทุกคนที่ใช้ REA รายงานบั๊ก เสนอฟีเจอร์ ทดสอบบิลด์ และช่วยแก้ไข
diff --git a/README_tr.md b/README_tr.md
index ff5ae32399..6bf9ffd0da 100644
--- a/README_tr.md
+++ b/README_tr.md
@@ -256,7 +256,7 @@ Güvenlik açıklarını [SECURITY.md](SECURITY.md) üzerinden bildirin.
## Yıldız geçmişi
-🎉 **GitHub'da 40.000 yıldız — teşekkürler!**
+🎉 **GitHub'da 50.000 yıldız — teşekkürler!**
REA'yı kullanan, hata bildiren, özellik talep eden, derlemeleri test eden ve düzeltmelere katkıda bulunan herkese teşekkürler.
diff --git a/README_uk.md b/README_uk.md
index 973ff9b417..c59b27becc 100644
--- a/README_uk.md
+++ b/README_uk.md
@@ -256,7 +256,7 @@ npx rea-agents@latest setup
## Історія зірок
-🎉 **40 000 зірок на GitHub — дякуємо!**
+🎉 **50 000 зірок на GitHub — дякуємо!**
Дякуємо всім, хто використовує REA, повідомляє про помилки, пропонує нові функції, тестує збірки й допомагає з виправленнями.
diff --git a/README_vi.md b/README_vi.md
index 6f198f90fc..cb33dd266c 100644
--- a/README_vi.md
+++ b/README_vi.md
@@ -256,7 +256,7 @@ Báo cáo lỗ hổng theo [SECURITY.md](SECURITY.md).
## Lịch sử sao
-🎉 **40.000 sao trên GitHub — xin cảm ơn!**
+🎉 **50.000 sao trên GitHub — xin cảm ơn!**
Cảm ơn mọi người đã sử dụng REA, báo lỗi, đề xuất tính năng, kiểm thử bản dựng và đóng góp bản sửa lỗi.
diff --git a/README_zh-TW.md b/README_zh-TW.md
index adef7444c9..ec44d4c3e0 100644
--- a/README_zh-TW.md
+++ b/README_zh-TW.md
@@ -258,7 +258,7 @@ npx rea-agents@latest setup
## Star 歷史
-🎉 **GitHub Star 達到 40,000 個,感謝大家!**
+🎉 **GitHub Star 達到 50,000 個,感謝大家!**
感謝每一位使用 REA、報告錯誤、提出功能需求、測試建置和貢獻修復的朋友。
diff --git a/README_zh.md b/README_zh.md
index baf67a1451..6dca111fb6 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -258,7 +258,7 @@ npx rea-agents@latest setup
## Star 历史
-🎉 **GitHub Star 达到 40,000 个,感谢大家!**
+🎉 **GitHub Star 达到 50,000 个,感谢大家!**
感谢每一位使用 REA、报告错误、提出功能需求、测试构建和贡献修复的朋友。
From 98022a835275375ee0ffa88b7c8983ba6f080040 Mon Sep 17 00:00:00 2001
From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com>
Date: Sat, 10 Oct 2026 12:02:24 +0800
Subject: [PATCH 13/74] fix(javascript)!: avoid endpoint payload copies in
graph metadata (#1423)
Keep exact endpoint observations and request properties while using compact display labels and collision-safe canonical value digests for long artifact-local endpoint keys. Verify real CLI/stdio MCP tracing and version comparison without truncating evidence.
BREAKING CHANGE: New analyses use compact endpoint labels and digest-based IDs for long or reserved-prefix endpoint keys. Select IDs from the parent graph and read full values from endpoint properties. Previously stored graphs retain their original identities and remain valid workflow inputs.
---
docs/javascript-application-workflows.md | 14 +
.../JavaScriptArtifactGraphFindings.ts | 19 +-
.../JavaScriptSemanticGraphDataProjection.ts | 6 +-
.../javascriptEndpointPayload.test.ts | 263 ++++++++++++++++++
4 files changed, 295 insertions(+), 7 deletions(-)
create mode 100644 tests/acceptance/applications/javascriptEndpointPayload.test.ts
diff --git a/docs/javascript-application-workflows.md b/docs/javascript-application-workflows.md
index 0093710351..592276e15e 100644
--- a/docs/javascript-application-workflows.md
+++ b/docs/javascript-application-workflows.md
@@ -104,6 +104,20 @@ graph and read literal values from `properties.value`, rather than decoding
role keys or labels. Previously stored graphs and their original IDs remain
valid inputs.
+Endpoint nodes keep the exact endpoint in each observation's `properties.value`;
+semantic request and response nodes keep it in `properties.endpoint`. Endpoint
+identity keys use `value-sha256:` over the canonical JSON string when
+that is shorter, or when the original value starts with the reserved digest
+prefix. Other short endpoint keys remain unchanged. Endpoint labels use the
+shorter of the exact value and ` endpoint`; semantic request/response
+labels use the shorter of the endpoint and operation method. These labels are
+display metadata, not endpoint lookup values.
+
+New analyses therefore change hashed endpoint IDs and longer endpoint labels.
+Use IDs from the selected graph and read the complete endpoint properties;
+previously stored graphs still support their original IDs. Literal endpoint
+search, semantic tracing, and version comparison use the preserved values.
+
## Version comparison
REA pairs entities only when a tier produces one unique candidate on each side.
diff --git a/src/application/javascript/JavaScriptArtifactGraphFindings.ts b/src/application/javascript/JavaScriptArtifactGraphFindings.ts
index cff140220a..580a4d0355 100644
--- a/src/application/javascript/JavaScriptArtifactGraphFindings.ts
+++ b/src/application/javascript/JavaScriptArtifactGraphFindings.ts
@@ -1,5 +1,6 @@
import { createHash } from "node:crypto";
+import { digestCanonicalValue } from "../../domain/canonicalDigest.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
@@ -162,16 +163,22 @@ const addEndpoint = (
const source =
sourceNodeFor(context, input.file.path, input.value.module_key) ??
input.asset;
+ const digestPrefix = "value-sha256:";
+ const valueKey = `${digestPrefix}${digestCanonicalValue(input.value.value, "JavaScript endpoint")}`;
+ // Reserve the digest prefix so an exact caller value cannot alias a digest key.
+ const key =
+ input.value.value.length <= valueKey.length &&
+ !input.value.value.startsWith(digestPrefix)
+ ? input.value.value
+ : valueKey;
+ const label = `${input.value.kind} endpoint`;
const node = context.accumulator.addNode({
kind: "endpoint",
- identity: artifactLocalIdentity(
- input.file.sha256,
- input.value.kind,
- input.value.value,
- ),
+ identity: artifactLocalIdentity(input.file.sha256, input.value.kind, key),
observations: [
{
- label: input.value.value,
+ label:
+ input.value.value.length <= label.length ? input.value.value : label,
properties: {
endpoint_kind: input.value.kind,
value: input.value.value,
diff --git a/src/application/javascript/JavaScriptSemanticGraphDataProjection.ts b/src/application/javascript/JavaScriptSemanticGraphDataProjection.ts
index 51efcd2ec6..b1c44e8f8c 100644
--- a/src/application/javascript/JavaScriptSemanticGraphDataProjection.ts
+++ b/src/application/javascript/JavaScriptSemanticGraphDataProjection.ts
@@ -137,7 +137,11 @@ const addRequestNode = (
kind: operation.kind === "request" ? "request" : "response",
roleKey: operation.requestId,
location: operation.location,
- label: operation.endpoint ?? operation.method,
+ label:
+ operation.endpoint !== null &&
+ operation.endpoint.length <= operation.method.length
+ ? operation.endpoint
+ : operation.method,
functionNodeId:
operation.ownerCallableId === null
? null
diff --git a/tests/acceptance/applications/javascriptEndpointPayload.test.ts b/tests/acceptance/applications/javascriptEndpointPayload.test.ts
new file mode 100644
index 0000000000..6a980aa7ad
--- /dev/null
+++ b/tests/acceptance/applications/javascriptEndpointPayload.test.ts
@@ -0,0 +1,263 @@
+import { Client } from "@modelcontextprotocol/client";
+import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
+import { createHash } from "node:crypto";
+import { writeFile } from "node:fs/promises";
+import { join, resolve } from "node:path";
+import { expect } from "vitest";
+
+import { toolContract } from "../../../src/contracts/toolContracts.js";
+import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
+import { applicationVersionComparisonResultSchema } from "../../../src/domain/javascript/javascriptApplicationVersionComparisonSchemas.js";
+import { applicationFeatureTraceResultSchema } from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js";
+import { javaScriptSemanticTraceResultSchema } from "../../../src/domain/javascript/javascriptSemanticTraceSchemas.js";
+import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
+import { cliTest } from "../../support/cli/cliFixture.js";
+
+cliTest(
+ "keeps full endpoint evidence usable without copying large values into metadata",
+ async ({ cli }) => {
+ const root = await createTestTempDirectory("rea-endpoint-payload-");
+ const endpoint = `https://example.invalid/${"part/ğ".repeat(160_000)}`;
+ const digestLookalike = `value-sha256:${createHash("sha256").update(JSON.stringify(endpoint)).digest("hex")}`;
+ // UTF-8 replacement would conflate these distinct JavaScript strings.
+ const surrogate = "\ud800".repeat(100);
+ const replacement = "\ufffd".repeat(100);
+ const values = [
+ endpoint,
+ digestLookalike,
+ "/ok",
+ "/ok",
+ surrogate,
+ replacement,
+ ];
+ const source = values
+ .map((value) => `fetch(${JSON.stringify(value)});`)
+ .join("\n");
+ const sourcePath = join(root, "main.js");
+ await writeFile(sourcePath, source);
+ const cliResponse = await cli.run({
+ arguments: [
+ "analyze-javascript-application",
+ root,
+ "--artifact-format",
+ "directory",
+ "--json",
+ ],
+ timeoutMs: 60_000,
+ });
+ expect(cliResponse.exitCode).toBe(0);
+ const cliEvidence = toolContract(
+ "analyze_javascript_application",
+ ).outputSchema.parse(cliResponse.json);
+
+ const client = new Client({ name: "endpoint-payload-e2e", version: "1" });
+ const transport = new StdioClientTransport({
+ command: process.execPath,
+ args: [resolve("scripts/rea.mjs"), "mcp"],
+ cwd: process.cwd(),
+ env: { PATH: process.env.PATH ?? "" },
+ stderr: "pipe",
+ });
+ transport.stderr?.on("data", () => undefined);
+ try {
+ await client.connect(transport);
+ const response = await client.callTool({
+ name: "analyze_javascript_application",
+ arguments: { input_path: root, format: "directory" },
+ });
+ expect(response.isError).not.toBe(true);
+ const evidence = toolContract(
+ "analyze_javascript_application",
+ ).outputSchema.parse(response.structuredContent);
+ expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
+ const text = response.content.find((item) => item.type === "text");
+ if (text?.type !== "text") throw new Error("Missing MCP text result");
+ const textEvidence = toolContract(
+ "analyze_javascript_application",
+ ).outputSchema.parse(JSON.parse(text.text));
+ expect(textEvidence.evidence_id).toBe(evidence.evidence_id);
+ const result = javascriptApplicationAnalysisResultSchema.parse(
+ evidence.normalized_result,
+ );
+ const endpoints = result.graph.nodes.filter(
+ (node) => node.kind === "endpoint",
+ );
+ const nodeFor = (value: string) => {
+ const node = endpoints.find((candidate) =>
+ candidate.observations.some(
+ ({ properties }) => properties.value === value,
+ ),
+ );
+ if (node === undefined) throw new Error("Complete endpoint was lost");
+ return node;
+ };
+ const endpointNode = nodeFor(endpoint);
+ expect(
+ new Set(
+ [endpoint, digestLookalike, surrogate, replacement].map(
+ (value) => nodeFor(value).node_id,
+ ),
+ ).size,
+ ).toBe(4);
+ const repeatedNode = nodeFor("/ok");
+ expect(
+ endpoints.filter((node) =>
+ node.observations.some(
+ ({ properties }) => properties.value === "/ok",
+ ),
+ ),
+ ).toHaveLength(1);
+ expect(repeatedNode.observations).toHaveLength(2);
+ expect(repeatedNode.identity).toMatchObject({ key: "/ok" });
+ expect(
+ repeatedNode.observations.every(({ label }) => label === "/ok"),
+ ).toBe(true);
+ expect(endpointNode.identity).toMatchObject({
+ artifact_sha256: createHash("sha256").update(source).digest("hex"),
+ });
+ expect(endpointNode.observations[0]?.evidence.location).toMatchObject({
+ available: true,
+ value: { kind: "source-range", source: "main.js" },
+ });
+ const requests = result.semantic_graph.nodes.filter(
+ (node) =>
+ node.kind === "request" && node.properties.endpoint === endpoint,
+ );
+ expect(requests).toHaveLength(1);
+
+ await verifyEndpointWorkflows({
+ client,
+ evidenceId: evidence.evidence_id,
+ endpoint,
+ endpointNodeId: endpointNode.node_id,
+ requestNodeIds: requests.map(({ node_id }) => node_id),
+ root,
+ source,
+ sourcePath,
+ });
+ await client.ping();
+ } finally {
+ await client.close();
+ await transport.close();
+ }
+ },
+ 120_000,
+);
+
+interface EndpointWorkflowInput {
+ readonly client: Client;
+ readonly evidenceId: string;
+ readonly endpoint: string;
+ readonly endpointNodeId: string;
+ readonly requestNodeIds: readonly string[];
+ readonly root: string;
+ readonly source: string;
+ readonly sourcePath: string;
+}
+
+const verifyEndpointWorkflows = async ({
+ client,
+ evidenceId,
+ endpoint,
+ endpointNodeId,
+ requestNodeIds,
+ root,
+ source,
+ sourcePath,
+}: EndpointWorkflowInput): Promise => {
+ const application = {
+ kind: "retained-evidence",
+ evidence_id: evidenceId,
+ };
+ const featureResponse = await client.callTool({
+ name: "trace_application_feature",
+ arguments: {
+ application,
+ seed: {
+ kind: "string",
+ value: endpoint,
+ match: "exact",
+ case_sensitive: true,
+ },
+ },
+ });
+ expect(featureResponse.isError).not.toBe(true);
+ const featureEvidence = toolContract(
+ "trace_application_feature",
+ ).outputSchema.parse(featureResponse.structuredContent);
+ const feature = applicationFeatureTraceResultSchema.parse(
+ featureEvidence.normalized_result,
+ );
+ expect(feature.seed_matches.map(({ node_id }) => node_id)).toContain(
+ endpointNodeId,
+ );
+
+ const traceResponse = await client.callTool({
+ name: "trace_javascript_semantics",
+ arguments: {
+ application,
+ query: {
+ seed: { kind: "endpoint", value: endpoint },
+ direction: "backward-provenance",
+ },
+ },
+ });
+ expect(traceResponse.isError).not.toBe(true);
+ const traceEvidence = toolContract(
+ "trace_javascript_semantics",
+ ).outputSchema.parse(traceResponse.structuredContent);
+ const trace = javaScriptSemanticTraceResultSchema.parse(
+ traceEvidence.normalized_result,
+ );
+ expect(trace.seed_node_ids).toEqual(requestNodeIds);
+ expect(
+ trace.nodes.some((node) => node.properties.endpoint === endpoint),
+ ).toBe(true);
+
+ await writeFile(sourcePath, `// next artifact version\n${source}`);
+ const nextResponse = await client.callTool({
+ name: "analyze_javascript_application",
+ arguments: { input_path: root, format: "directory" },
+ });
+ expect(nextResponse.isError).not.toBe(true);
+ const nextEvidence = toolContract(
+ "analyze_javascript_application",
+ ).outputSchema.parse(nextResponse.structuredContent);
+ const next = javascriptApplicationAnalysisResultSchema.parse(
+ nextEvidence.normalized_result,
+ );
+ const nextEndpoint = next.graph.nodes.find(
+ (node) =>
+ node.kind === "endpoint" &&
+ node.observations.some(({ properties }) => properties.value === endpoint),
+ );
+ if (nextEndpoint === undefined) throw new Error("Next endpoint was lost");
+ const comparisonResponse = await client.callTool({
+ name: "compare_application_versions",
+ arguments: {
+ left: application,
+ right: {
+ kind: "retained-evidence",
+ evidence_id: nextEvidence.evidence_id,
+ },
+ },
+ });
+ expect(comparisonResponse.isError).not.toBe(true);
+ const comparisonEvidence = toolContract(
+ "compare_application_versions",
+ ).outputSchema.parse(comparisonResponse.structuredContent);
+ const comparison = applicationVersionComparisonResultSchema.parse(
+ comparisonEvidence.normalized_result,
+ );
+ expect(comparison.items).toContainEqual(
+ expect.objectContaining({
+ node_kind: "endpoint",
+ left_node_id: endpointNodeId,
+ right_node_id: nextEndpoint.node_id,
+ match: expect.objectContaining({
+ status: "matched",
+ basis: "semantic-key",
+ }),
+ }),
+ );
+};
From 28595276faac6a01dd4930313a91fab717d29513 Mon Sep 17 00:00:00 2001
From: Marnix Pluim
Date: Sat, 10 Oct 2026 06:03:13 +0200
Subject: [PATCH 14/74] fix(javascript): bound member-mutation paths through
reassignment loops
Fixes #1401. Refs #1065. Preserve exact member facts and equal-depth write effects while bounding the observed reassignment-loop expansion.
Validated with the JavaScript semantic/application suite, focused alias regressions, full static checks, real compiled CLI and SDK MCP Notion workflows, and all 20 checks on the final PR head.
Co-Authored-By: Marnix Pluim <286664873+marnixpluim@users.noreply.github.com>
Co-Authored-By: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
Co-Authored-By: Claude
---
...manticAnalysis.reference_lifetimes.test.ts | 157 ++++++++++
.../javascriptSemanticMemberMutations.ts | 270 +++++++++++++++++-
.../javascriptSemanticPropertyPaths.ts | 86 ++++++
.../javascript/javascriptSemanticValues.ts | 25 +-
4 files changed, 524 insertions(+), 14 deletions(-)
diff --git a/src/domain/javascript/javascriptSemanticAnalysis.reference_lifetimes.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.reference_lifetimes.test.ts
index 2795427bd4..73c7897b0f 100644
--- a/src/domain/javascript/javascriptSemanticAnalysis.reference_lifetimes.test.ts
+++ b/src/domain/javascript/javascriptSemanticAnalysis.reference_lifetimes.test.ts
@@ -9,6 +9,163 @@ const resultValue = (body: string) =>
"result",
).returnSites[0]?.value;
+describe("bounded member path propagation", () => {
+ it("bounds cyclic member paths with no observed container origin", () => {
+ const branches = Array.from(
+ { length: 16 },
+ (_, index) => `if (flag === ${index}) node = node.m${index};`,
+ ).join(" else ");
+ const start = performance.now();
+ const callable = onlyCallable(
+ analyzeJavaScriptSemantics(
+ `export function result(seed) { let node=seed; while(node) { ${branches} use(node.state); } return seed.keep; }`,
+ ),
+ "result",
+ );
+ expect(callable.returnSites[0]?.value.status).toBe("unknown");
+ expect(performance.now() - start).toBeLessThan(2000);
+ }, 30000);
+
+ it("preserves Array prototype effects through an otherwise unknown alias", () => {
+ expect(
+ resultValue(
+ "const values=[1]; const prototype=Array.prototype; prototype[Symbol.iterator]=custom; for(const item of values) { mutate(item); } return values[0];",
+ )?.status,
+ ).toBe("unknown");
+ });
+
+ it("bounds loops derived from a scalar member while preserving sibling facts", () => {
+ const branches = Array.from(
+ { length: 16 },
+ (_, index) => `if (flag === ${index}) node = node.m${index};`,
+ ).join(" else ");
+ const start = performance.now();
+ expect(
+ resultValue(
+ `const source={child:1,keep:{value:3}}; let node=source.child; while(node) { ${branches} use(node.state); } return source.keep.value;`,
+ ),
+ ).toEqual({ status: "literal", value: 3 });
+ expect(performance.now() - start).toBeLessThan(2000);
+ }, 30000);
+
+ it.each([
+ ["child:external,", "child", "present"],
+ ["", "missing", "unknown-coverage"],
+ ])(
+ "retains slot presence below an unobserved container: %s",
+ (property, name, presence) => {
+ expect(
+ resultValue(
+ `const source={${property}keep:3}; const alias=source.${name}; alias.deep.value=2; return source;`,
+ ),
+ ).toMatchObject({
+ status: "object",
+ properties: expect.arrayContaining([
+ {
+ name,
+ presence,
+ value: {
+ status: "unknown",
+ reason: "This property may have been mutated.",
+ },
+ },
+ {
+ name: "keep",
+ presence: "present",
+ value: { status: "literal", value: 3 },
+ },
+ ]),
+ });
+ },
+ );
+
+ it.each([
+ ["use(node);", "m0.value", "unknown"],
+ ["node.value = 2;", "m0.value", "unknown"],
+ ["node.value = 2;", "mode", "literal"],
+ ])(
+ "bounds a loop that reassigns one reference through many members: %s seed.%s",
+ (effect, read, status) => {
+ const branches = Array.from(
+ { length: 9 },
+ (_, index) => `if (flag === ${index}) node = node.m${index};`,
+ ).join(" else ");
+ const start = performance.now();
+ const value = resultValue(
+ `const seed = { mode: "initial", m0: { value: 1 } }; let node = seed; while (node) { ${branches} ${effect} } return seed.${read};`,
+ );
+ expect(performance.now() - start).toBeLessThan(2000);
+ expect(value?.status).toBe(status);
+ },
+ 30000,
+ );
+
+ it.each([
+ ["const alias = flag ? owner.child : owner; alias.value = 2;", "value"],
+ ["const alias = flag ? owner : owner.child; alias.value = 2;", "value"],
+ ["const alias = flag ? owner.child : owner; alias[key] = 2;", "value"],
+ [
+ "const alias = flag ? owner.child : owner; alias.child = 2;",
+ "child.value",
+ ],
+ [
+ "let alias = owner; let i = 0; while (i++ < 1) { alias = flag ? alias.child : alias; alias.value = 2; }",
+ "value",
+ ],
+ ])(
+ "keeps a write that reaches a child through a longer path: %s",
+ (body, read) => {
+ expect(
+ resultValue(
+ `const shared = { value: 1, child: { value: 1 } }; const owner = { child: shared }; ${body} return shared.${read};`,
+ )?.status,
+ ).toBe("unknown");
+ },
+ );
+
+ it.each([
+ "mutate(source.left, source.right);",
+ "const alias = flag ? source.left : source.right; mutate(alias);",
+ "const copy = flag ? [source.left] : [source.right]; mutate(copy);",
+ "const alias = flag ? source.left : source.right; alias.value = 2;",
+ ])("keeps members outside the mutated paths: %s", (body) => {
+ expect(
+ resultValue(
+ `const source = { left: { value: 1 }, right: { value: 2 }, keep: { value: 3 } }; ${body} return source.keep.value;`,
+ ),
+ ).toEqual({ status: "literal", value: 3 });
+ });
+});
+
+describe("distinct observable mutation paths", () => {
+ it.each([
+ ["alias.value = 2;", "left"],
+ ["alias.value = 2;", "right"],
+ ["mutate(alias);", "left"],
+ ["mutate(alias);", "right"],
+ ])(
+ "propagates both conditional paths to shared bindings: %s %s",
+ (effect, target) => {
+ expect(
+ resultValue(
+ `const left={value:1}; const right={value:2}; const source={left,right}; const alias=flag?source.left:source.right; ${effect} return ${target}.value;`,
+ )?.status,
+ ).toBe("unknown");
+ },
+ );
+
+ it.each(["mutate(alias);", "alias.value = 2;"])(
+ "preserves sibling facts below a shared prefix: %s",
+ (effect) => {
+ expect(
+ resultValue(
+ `const shared={left:{value:1},right:{value:2},keep:{value:3}}; const source={nested:shared}; const alias=flag?source.nested.left:source.nested.right; ${effect} return shared.keep.value;`,
+ ),
+ ).toEqual({ status: "literal", value: 3 });
+ },
+ );
+});
+
describe("reference lifetimes across calls and shallow copies", () => {
it("bounds dynamic destructuring across shared alias branches", () => {
const declarations = Array.from(
diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts
index ae887b9c79..21a6aded04 100644
--- a/src/domain/javascript/javascriptSemanticMemberMutations.ts
+++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts
@@ -18,6 +18,7 @@ import { semanticIterationSources } from "./javascriptSemanticIterationSources.j
import { semanticMutationInitializers } from "./javascriptSemanticMutationInitializers.js";
import {
semanticArrayIndex,
+ SemanticPropertyPathCoverage,
semanticPropertyPathKeyMatches,
type JavaScriptSemanticPropertyPath,
} from "./javascriptSemanticPropertyPaths.js";
@@ -42,8 +43,10 @@ export const collectSemanticMemberMutations = (
if (parent !== null) parents.set(node, parent);
},
});
+ const normalizePath = observableMutationPaths(state);
const recordedEffects = new Set();
const selections = new Map();
+ const coveringPaths = new Map();
let arrayIterationUnknown = false;
const pendingReferences: {
readonly initializer: JavaScriptSemanticBindingState["referenceInitializers"][number];
@@ -93,8 +96,10 @@ export const collectSemanticMemberMutations = (
originAt: t.Node | undefined = mutation,
): void => {
const pending = [{ node, path, bindings, originAt }];
- while (pending.length > 0) {
- const current = pending.pop();
+ // Visit shallower alias traversals first so their exact prefixes suppress
+ // longer loop paths before those paths enumerate every assignment order.
+ for (let cursor = 0; cursor < pending.length; cursor++) {
+ const current = pending[cursor];
if (current === undefined) break;
const expression = unwrapJavaScriptExpression(current.node).node;
if (t.isIdentifier(expression)) {
@@ -126,10 +131,13 @@ export const collectSemanticMemberMutations = (
if (current.bindings.has(lifetime)) continue;
// Union compatible copied-slot selections at each binding. Carrying every
// combination of exclusions through branch diamonds is exponential.
- const selectorIndex = current.path.findIndex(
+ // Preserve paths that can change observed slots or Array prototype
+ // effects; canonicalize suffixes that cannot distinguish lattice facts.
+ let path = normalizePath(binding.bindingId, current.path, effect);
+ if (path === null) continue;
+ const selectorIndex = path.findIndex(
(key) => key !== null && typeof key === "object",
);
- let path = current.path;
const selector = path[selectorIndex];
if (selector !== null && typeof selector === "object") {
const selectionIdentity = JSON.stringify([
@@ -157,6 +165,25 @@ export const collectSemanticMemberMutations = (
index === selectorIndex ? { ...selector, excludedKeys } : key,
);
}
+ // Keep an antichain of exact paths. Widening distinct member keys would
+ // also invalidate unrelated siblings that never reach this effect.
+ const pathState = JSON.stringify([
+ binding.bindingId,
+ effect,
+ // Short writes stop at owned literal slots, whereas longer writes
+ // reach shared children. Only equal-depth writes cover one another.
+ effect === "write" ? path.length : null,
+ mutation?.start,
+ mutation?.end,
+ current.originAt?.start,
+ current.originAt?.end,
+ ]);
+ let covering = coveringPaths.get(pathState);
+ if (covering === undefined) {
+ covering = new SemanticPropertyPathCoverage();
+ coveringPaths.set(pathState, covering);
+ }
+ if (!covering.retain(path)) continue;
const identity = JSON.stringify([
binding.bindingId,
effect,
@@ -544,6 +571,241 @@ const referencedValues = (
return [];
};
+type MutationObservableOrigin =
+ | t.ObjectExpression
+ | t.ArrayExpression
+ | "ambient"
+ | "array"
+ | "array-prototype"
+ | "opaque";
+
+interface MutationObserverNode {
+ readonly origins: Set;
+ readonly sources: Set;
+ readonly dependents: Set;
+ readonly projections: Map;
+ readonly read?: {
+ readonly source: MutationObserverNode;
+ readonly key: PropertyPath[number];
+ };
+}
+
+const observableMutationPaths = (
+ state: JavaScriptSemanticAnalysisState,
+): ((
+ bindingId: string,
+ path: PropertyPath,
+ effect: ValueEffect,
+) => PropertyPath | null) => {
+ const bindingNodes = new Map();
+ const expressions = new WeakMap();
+ const pendingExpressions: {
+ readonly expression: t.Node;
+ readonly observer: MutationObserverNode;
+ }[] = [];
+ const pending: MutationObserverNode[] = [];
+ const queued = new Set();
+ const schedule = (node: MutationObserverNode): void => {
+ if (queued.has(node)) return;
+ queued.add(node);
+ pending.push(node);
+ };
+ const create = (
+ read?: MutationObserverNode["read"],
+ ): MutationObserverNode => ({
+ origins: new Set(),
+ sources: new Set(),
+ dependents: new Set(),
+ projections: new Map(),
+ ...(read === undefined ? {} : { read }),
+ });
+ const link = (
+ source: MutationObserverNode,
+ target: MutationObserverNode,
+ ): void => {
+ if (target.sources.has(source)) return;
+ target.sources.add(source);
+ source.dependents.add(target);
+ schedule(target);
+ };
+ const expressionNode = (node: t.Node): MutationObserverNode => {
+ const expression = unwrapJavaScriptExpression(node).node;
+ const existing = expressions.get(expression);
+ if (existing !== undefined) return existing;
+ const observer = create();
+ expressions.set(expression, observer);
+ pendingExpressions.push({ expression, observer });
+ return observer;
+ };
+ const project = (
+ source: MutationObserverNode,
+ path: PropertyPath,
+ ): MutationObserverNode => {
+ let observer = source;
+ for (const key of path) {
+ const identity = JSON.stringify(
+ key === null || typeof key === "object" ? key : String(key),
+ );
+ let selected = observer.projections.get(identity);
+ if (selected === undefined) {
+ selected = create({ source: observer, key });
+ observer.projections.set(identity, selected);
+ observer.dependents.add(selected);
+ schedule(selected);
+ }
+ observer = selected;
+ }
+ return observer;
+ };
+ for (const binding of state.bindingsById.values())
+ bindingNodes.set(binding.bindingId, create());
+ for (const binding of state.bindingsById.values()) {
+ const observer = bindingNodes.get(binding.bindingId);
+ if (observer === undefined) continue;
+ for (const initializer of binding.initializers)
+ link(
+ project(expressionNode(initializer.node), initializer.projection),
+ observer,
+ );
+ // Preserve exact paths where copies, defaults or iteration need their
+ // reference-specific projections; an opaque origin prevents pruning.
+ if (binding.referenceInitializers.length > 0)
+ observer.origins.add("opaque");
+ for (const initializer of binding.referenceInitializers) {
+ for (const source of [
+ initializer,
+ ...(initializer.requiredSources ?? []),
+ ...(initializer.fallbackSources ?? []),
+ ])
+ link(expressionNode(source.node), observer);
+ }
+ }
+ const settle = (): void => {
+ while (pendingExpressions.length > 0 || pending.length > 0) {
+ while (pendingExpressions.length > 0) {
+ const selected = pendingExpressions.pop();
+ if (selected === undefined) break;
+ const { expression, observer } = selected;
+ if (t.isObjectExpression(expression) || t.isArrayExpression(expression))
+ observer.origins.add(expression);
+ else if (t.isIdentifier(expression)) {
+ const binding = resolveSemanticBindingState(
+ state,
+ expression,
+ expression.name,
+ );
+ if (binding !== undefined) {
+ const origin = bindingNodes.get(binding.bindingId);
+ if (origin !== undefined) link(origin, observer);
+ } else if (expression.name === "Array") observer.origins.add("array");
+ else if (
+ ["globalThis", "global", "window", "self"].includes(expression.name)
+ )
+ observer.origins.add("ambient");
+ } else if (
+ t.isMemberExpression(expression) ||
+ t.isOptionalMemberExpression(expression)
+ )
+ link(
+ project(expressionNode(expression.object), [
+ semanticStaticPropertyKey(
+ expression.property,
+ expression.computed,
+ ),
+ ]),
+ observer,
+ );
+ else
+ for (const reference of referencedValues(expression, [], "escape"))
+ link(
+ project(expressionNode(reference.node), reference.path),
+ observer,
+ );
+ schedule(observer);
+ }
+ const observer = pending.pop();
+ if (observer === undefined) continue;
+ queued.delete(observer);
+ let changed = false;
+ const add = (origin: MutationObservableOrigin): void => {
+ if (observer.origins.has(origin)) return;
+ observer.origins.add(origin);
+ changed = true;
+ };
+ for (const source of observer.sources)
+ for (const origin of source.origins) add(origin);
+ if (observer.read !== undefined) {
+ const { source, key } = observer.read;
+ if (key === null || typeof key === "object") {
+ // Dynamic keys and copied-slot masks retain their original paths.
+ // Enumerating their exclusion combinations would recreate the growth
+ // that the mutation collector's selection union already prevents.
+ if (source.origins.size > 0) add("opaque");
+ } else
+ for (const origin of source.origins) {
+ if (typeof origin === "string") {
+ if (origin === "array-prototype" || origin === "opaque")
+ add(origin);
+ else if (
+ origin === "ambient" &&
+ semanticPropertyPathKeyMatches(key, "Array")
+ )
+ add("array");
+ else if (
+ origin === "array" &&
+ semanticPropertyPathKeyMatches(key, "prototype")
+ )
+ add("array-prototype");
+ } else {
+ for (const reference of referencedValues(
+ origin,
+ [key],
+ "escape",
+ )) {
+ link(
+ project(expressionNode(reference.node), reference.path),
+ observer,
+ );
+ if (reference.iterableFallbackPath !== undefined)
+ link(
+ project(
+ expressionNode(reference.node),
+ reference.iterableFallbackPath,
+ ),
+ observer,
+ );
+ }
+ }
+ }
+ }
+ if (changed)
+ for (const dependent of observer.dependents) schedule(dependent);
+ }
+ };
+ settle();
+ return (bindingId, path, effect) => {
+ let observer = bindingNodes.get(bindingId);
+ if (observer === undefined) return path;
+ if (path.some((key) => key !== null && typeof key === "object"))
+ return path;
+ if (effect === "escape") {
+ observer = project(observer, path);
+ settle();
+ return observer.origins.size === 0 ? null : path;
+ }
+ if (observer.origins.size === 0) return path.length === 0 ? path : [null];
+ for (let offset = 0; offset < path.length; offset++) {
+ const key = path[offset];
+ if (key === undefined) break;
+ observer = project(observer, [key]);
+ settle();
+ if (observer.origins.size === 0 && offset + 1 < path.length)
+ return [...path.slice(0, offset + 1), null];
+ }
+ return path;
+ };
+};
+
const objectReferencedValues = (
node: t.ObjectExpression,
path: PropertyPath,
diff --git a/src/domain/javascript/javascriptSemanticPropertyPaths.ts b/src/domain/javascript/javascriptSemanticPropertyPaths.ts
index 9bee0e9b7a..95cbeef974 100644
--- a/src/domain/javascript/javascriptSemanticPropertyPaths.ts
+++ b/src/domain/javascript/javascriptSemanticPropertyPaths.ts
@@ -35,3 +35,89 @@ export const semanticPropertyPathKeyMatches = (
const index = semanticArrayIndex(name);
return index !== null && index >= key.startIndex;
};
+
+interface PropertyPathCoverageNode {
+ terminal: boolean;
+ readonly exact: Map;
+ readonly selectors: Map<
+ string,
+ {
+ readonly key: JavaScriptSemanticPropertyPath[number];
+ readonly node: PropertyPathCoverageNode;
+ }
+ >;
+}
+
+const coverageNode = (): PropertyPathCoverageNode => ({
+ terminal: false,
+ exact: new Map(),
+ selectors: new Map(),
+});
+
+/** Index exact effect prefixes without widening distinct member paths. */
+export class SemanticPropertyPathCoverage {
+ readonly #root = coverageNode();
+
+ /** Retain a path only when no previously retained prefix covers it. */
+ retain(path: JavaScriptSemanticPropertyPath): boolean {
+ const pending = [{ node: this.#root, offset: 0 }];
+ while (pending.length > 0) {
+ const current = pending.pop();
+ if (current === undefined) break;
+ if (current.node.terminal) return false;
+ const key = path[current.offset];
+ if (key === undefined) continue;
+ if (key !== null && typeof key !== "object") {
+ const exact = current.node.exact.get(String(key));
+ if (exact !== undefined)
+ pending.push({ node: exact, offset: current.offset + 1 });
+ }
+ for (const selected of current.node.selectors.values()) {
+ if (keyCovers(selected.key, key))
+ pending.push({ node: selected.node, offset: current.offset + 1 });
+ }
+ }
+ let node = this.#root;
+ for (const key of path) {
+ if (key !== null && typeof key !== "object") {
+ const name = String(key);
+ let child = node.exact.get(name);
+ if (child === undefined) {
+ child = coverageNode();
+ node.exact.set(name, child);
+ }
+ node = child;
+ } else {
+ const identity = JSON.stringify(key);
+ let selected = node.selectors.get(identity);
+ if (selected === undefined) {
+ selected = { key, node: coverageNode() };
+ node.selectors.set(identity, selected);
+ }
+ node = selected.node;
+ }
+ }
+ node.terminal = true;
+ node.exact.clear();
+ node.selectors.clear();
+ return true;
+ }
+}
+
+const keyCovers = (
+ covering: JavaScriptSemanticPropertyPath[number],
+ key: JavaScriptSemanticPropertyPath[number],
+): boolean => {
+ if (covering === null) return true;
+ if (key === null) return false;
+ if (typeof key !== "object")
+ return typeof covering === "object"
+ ? semanticPropertyPathKeyMatches(covering, String(key))
+ : String(covering) === String(key);
+ return (
+ typeof covering === "object" &&
+ covering.excludedKeys.every((name) => key.excludedKeys.includes(name)) &&
+ (covering.startIndex === undefined ||
+ (key.startIndex !== undefined && covering.startIndex <= key.startIndex))
+ );
+};
diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts
index 2f53626708..884915948c 100644
--- a/src/domain/javascript/javascriptSemanticValues.ts
+++ b/src/domain/javascript/javascriptSemanticValues.ts
@@ -211,12 +211,17 @@ const evaluateBinding = (
if (isPrimitive(captured)) value = captured;
}
}
- const mutated = binding.mutatedPaths.reduce(
- invalidateSemanticMutationPath,
- value,
- );
- const projected = binding.escapedPaths.reduce(
- (value, escape) =>
+ let projected = value;
+ for (const path of binding.mutatedPaths) {
+ projected = invalidateSemanticMutationPath(projected, path);
+ // Every subsequent write returns this same root unknown. Replaying those
+ // paths at every newly collected effect otherwise becomes quadratic.
+ if (projected.status === "unknown") break;
+ }
+ for (const escape of binding.escapedPaths) {
+ // Escapes only invalidate object references; other lattice values survive.
+ if (projected.status !== "object" && projected.status !== "array") break;
+ if (
context.capturePoint !== undefined &&
semanticEscapeFollowsCapture(
binding,
@@ -224,10 +229,10 @@ const evaluateBinding = (
escape.node,
context.state.parentsByNode,
)
- ? value
- : invalidateSemanticEscapedPath(value, escape.path),
- mutated,
- );
+ )
+ continue;
+ projected = invalidateSemanticEscapedPath(projected, escape.path);
+ }
// Point-local memoization also bounds failed speculative captures. Only a
// primitive result is allowed back into ordinary value evaluation.
if (
From 4d95f2b2e7a6fdc1ba9dfcbd90578f6bbc5e5e1e Mon Sep 17 00:00:00 2001
From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com>
Date: Sat, 10 Oct 2026 12:15:11 +0800
Subject: [PATCH 15/74] fix(validation): retain unmatched union correction
details
Preserve actionable field paths and schema-declared variants for unknown or missing discriminators.
Closes #1421. Refs #1065.
---
src/domain/inputIssueProjection.ts | 19 ++++++++-
tests/boundary/cli/analysisViewCli.test.ts | 45 ++++++++++++++++++++++
2 files changed, 63 insertions(+), 1 deletion(-)
diff --git a/src/domain/inputIssueProjection.ts b/src/domain/inputIssueProjection.ts
index 25798721c9..1d5db83b65 100644
--- a/src/domain/inputIssueProjection.ts
+++ b/src/domain/inputIssueProjection.ts
@@ -35,7 +35,23 @@ const projectIssue = (
typeof part === "string" || typeof part === "number" ? [part] : [],
);
// Branch issue paths are relative to the union; branches often agree.
- if (issue.code === "invalid_union")
+ if (issue.code === "invalid_union") {
+ // Unmatched discriminators and ambiguous unions have no branch issues.
+ // Keep their outer failure instead of flattening it into an empty list.
+ if (issue.errors.length === 0) {
+ const expected =
+ "options" in issue ? issue.options?.filter(isSafeExpected) : undefined;
+ return [
+ {
+ path,
+ reason:
+ valueAtPath(input, path) === undefined
+ ? "missing_argument"
+ : "invalid_value",
+ ...(expected === undefined ? {} : { expected }),
+ },
+ ];
+ }
return uniqueIssues(
issue.errors.flatMap((branch) =>
branch.flatMap((branchIssue) =>
@@ -46,6 +62,7 @@ const projectIssue = (
),
),
);
+ }
if (issue.code === "invalid_key")
return issue.issues.flatMap((keyIssue) =>
projectIssue(
diff --git a/tests/boundary/cli/analysisViewCli.test.ts b/tests/boundary/cli/analysisViewCli.test.ts
index e6e6fb51d7..86e0d9b271 100644
--- a/tests/boundary/cli/analysisViewCli.test.ts
+++ b/tests/boundary/cli/analysisViewCli.test.ts
@@ -80,3 +80,48 @@ cliTest("rejects a request that omits source", async ({ cli }) => {
details: { operation: "inspect-analysis-view" },
});
});
+
+for (const [view, reason] of [
+ [{ kind: "unknown-view" }, "invalid_value"],
+ [{}, "missing_argument"],
+] as const) {
+ cliTest(
+ `reports correction details for ${reason} view kind`,
+ async ({ cli }) => {
+ const root = await createTestTempDirectory("rea-view-variant-");
+ const input = join(root, "view.json");
+ await writeFile(
+ input,
+ JSON.stringify({
+ source: {
+ kind: "inline",
+ evidence: analysisViewLayoutEvidence(),
+ },
+ view,
+ }),
+ );
+ const result = await cli.run({
+ arguments: ["inspect-analysis-view", input, "--json"],
+ environment: {
+ HOME: root,
+ XDG_CONFIG_HOME: root,
+ XDG_CACHE_HOME: root,
+ },
+ });
+ expect(result.exitCode).toBe(1);
+ expect(result.json).toMatchObject({
+ code: "invalid_request",
+ details: {
+ issues: [
+ {
+ path: ["view", "kind"],
+ reason,
+ expected: expect.arrayContaining(["summary", "page"]),
+ },
+ ],
+ },
+ });
+ expect(JSON.stringify(result.json)).not.toContain("unknown-view");
+ },
+ );
+}
From a9ee2360fd11332b2cc50ef7130a89321b828d28 Mon Sep 17 00:00:00 2001
From: jhuang-tw <77732008+jhuang-tw@users.noreply.github.com>
Date: Sat, 10 Oct 2026 12:18:50 +0800
Subject: [PATCH 16/74] feat(native): inspect oversized retained function
dossiers via bounded views
Add caller-selected native dossier facets with preserved Evidence identity, provenance, unknowns and transport recovery.
Refs #1247 and #1065.
Co-authored-by: jhuang-tw <77732008+jhuang-tw@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
---
docs/mcp-contracts.md | 7 +-
.../references/native-and-artifacts.md | 8 +
.../analysisView/AnalysisViewService.test.ts | 92 ++++++++
.../analysisView/AnalysisViewService.ts | 18 +-
src/cli/analysisViewCommands.ts | 2 +-
src/contracts/analysisViewToolContracts.ts | 19 +-
src/domain/analysisView/analysisView.test.ts | 197 +++++++++++++++--
src/domain/analysisView/analysisView.ts | 63 +++++-
src/domain/analysisView/binaryLayoutView.ts | 4 +
.../analysisView/javascriptApplicationView.ts | 4 +
src/domain/analysisView/nativeFunctionView.ts | 202 ++++++++++++++++++
tests/boundary/cli/analysisViewCli.test.ts | 44 ++++
tests/boundary/mcp/analysisViewMcp.test.ts | 131 +++++++++++-
13 files changed, 769 insertions(+), 22 deletions(-)
create mode 100644 src/domain/analysisView/nativeFunctionView.ts
diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md
index 5eedd9c14c..7bf2fb76c0 100644
--- a/docs/mcp-contracts.md
+++ b/docs/mcp-contracts.md
@@ -237,7 +237,12 @@ Ordinary responses keep their existing complete result contract.
## Retained application Evidence inputs
`inspect_analysis_view` projects a caller-selected view of already completed
-`inspect_binary_layout` or `analyze_javascript_application` Evidence. Source is
+`inspect_binary_layout`, `analyze_javascript_application`, or `analyze_function`
+Evidence. Native views select procedure, pseudocode, assembly, callers/callees,
+references, and high-pcode facets without starting a provider. Native offset
+and limit default to 0 and 64; pseudocode uses UTF-16 code units and never
+splits surrogate pairs, while other pages count rows. Unavailable facts and
+provider limitations remain explicit. Source is
an exact same-session retained reference or portable inline Evidence. Views are
a summary, a layout mitigations or linkage facet, one section/symbol/module, or
a stable page with a caller-selected positive `limit`. Module pages include
diff --git a/skill-src/reverse-engineer-anything/references/native-and-artifacts.md b/skill-src/reverse-engineer-anything/references/native-and-artifacts.md
index f47017171d..65b68c6b2d 100644
--- a/skill-src/reverse-engineer-anything/references/native-and-artifacts.md
+++ b/skill-src/reverse-engineer-anything/references/native-and-artifacts.md
@@ -34,6 +34,14 @@ cross-references. Addresses and recovered pseudocode are analysis observations,
not original source. Provider unavailability and unsupported metadata remain
unknown rather than false.
+For a completed `analyze_function` dossier, use `inspect_analysis_view` with
+`view: {kind: "native", facet: "value_flow_operations", offset: 0, limit: 32}`
+and the exact retained Evidence ID to read selected facts without reanalysis.
+Native facets also include procedure, pseudocode, assembly, callers/callees,
+references, and value-flow summaries. Pseudocode offsets and limits count UTF-16
+code units; use the returned `next_offset` for continuation. Provider limitations
+and unavailable facts remain explicit; the complete parent Evidence is retained.
+
To see which functions or Objective-C methods a Mach-O actually calls in one
run, use `observe_native_calls` with explicit breakpoints and a bounded
`duration_ms`/`max_events`. It launches a new process under LLDB, so the target
diff --git a/src/application/analysisView/AnalysisViewService.test.ts b/src/application/analysisView/AnalysisViewService.test.ts
index 29289efae8..b361e162fa 100644
--- a/src/application/analysisView/AnalysisViewService.test.ts
+++ b/src/application/analysisView/AnalysisViewService.test.ts
@@ -2,6 +2,8 @@ import { expect, it } from "vitest";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
import { createEvidence, parseEvidence } from "../../domain/evidence.js";
+import { functionDossierSchema } from "../../domain/hopperValues.js";
+import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js";
import {
analysisViewJavaScriptAnalysisWithSource,
analysisViewJavaScriptEvidence,
@@ -9,6 +11,96 @@ import {
} from "../../../tests/fixtures/analysisView.js";
import { inspectAnalysisView } from "./AnalysisViewService.js";
+it.each(["/fixtures/unknown.exe", ""])(
+ "preserves recorded empty native identity fields: path=%j",
+ (path) => {
+ const original = functionDossierSchema.parse(ghidraFunctionDossier());
+ const parent = createEvidence(
+ { path, format: "pe", sha256: "c".repeat(64) },
+ { id: "ghidra", name: "Ghidra", version: "12.1.4" },
+ {
+ operation: "analyze_function",
+ parameters: {},
+ result: {
+ ...original,
+ procedure: {
+ ...original.procedure,
+ address: "",
+ body: { available: false, reason: "Not recorded" },
+ },
+ },
+ },
+ );
+ const result = inspectAnalysisView({
+ source: { kind: "inline", evidence: parent },
+ view: {
+ kind: "native",
+ facet: "procedure",
+ offset: 0,
+ limit: 64,
+ },
+ });
+ if (!result.ok) throw result.error;
+ expect(result.value.normalized_result).toMatchObject({
+ procedure_address: "",
+ artifact: { path },
+ item: { address: "" },
+ });
+ expect(result.value.locations).toEqual(
+ path.length > 0 ? [{ kind: "artifact-path", path }] : [],
+ );
+ },
+);
+
+it("projects a small native view from an authenticated >10 MiB retained dossier", () => {
+ const original = functionDossierSchema.parse(ghidraFunctionDossier());
+ const parent = createEvidence(
+ { path: "/fixtures/large.exe", format: "pe", sha256: "c".repeat(64) },
+ { id: "ghidra", name: "Ghidra", version: "12.1.4" },
+ {
+ operation: "analyze_function",
+ parameters: { address: "0x401000" },
+ result: { ...original, pseudocode: "X".repeat(11 * 1024 * 1024) },
+ limitations: ["Synthetic fixture"],
+ },
+ );
+ const input = {
+ source: {
+ kind: "retained-evidence" as const,
+ evidence_id: parent.evidence_id,
+ },
+ view: {
+ kind: "native" as const,
+ facet: "pseudocode" as const,
+ offset: 0,
+ limit: 128,
+ },
+ };
+ const view = inspectAnalysisView(input, (id) =>
+ id === parent.evidence_id ? parent : undefined,
+ );
+ if (!view.ok) throw view.error;
+ expect(view.value.evidence_links).toEqual([parent.evidence_id]);
+ expect(view.value.normalized_result).toMatchObject({
+ kind: "native",
+ parent_operation: "analyze_function",
+ parent_evidence_id: parent.evidence_id,
+ procedure_address: "0x401000",
+ artifact: { path: "/fixtures/large.exe", sha256: "c".repeat(64) },
+ coverage: { total: 11 * 1024 * 1024, examined: 128, next_offset: 128 },
+ });
+ expect(view.value.locations).toEqual([
+ { kind: "artifact-path", path: "/fixtures/large.exe" },
+ { kind: "address", address: "0x401000" },
+ ]);
+ expect(JSON.stringify(view.value).length).toBeLessThan(12000);
+ expect(JSON.stringify(parent).length).toBeGreaterThan(10 * 1024 * 1024);
+ expect(inspectAnalysisView(input, () => undefined)).toMatchObject({
+ ok: false,
+ error: { _tag: "EvidenceIntegrityError" },
+ });
+});
+
it("projects inline layout Evidence and retains a derived view record", () => {
const parent = analysisViewLayoutEvidence();
const result = inspectAnalysisView({
diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts
index 0d10d5fa8d..7af6fb24a7 100644
--- a/src/application/analysisView/AnalysisViewService.ts
+++ b/src/application/analysisView/AnalysisViewService.ts
@@ -53,6 +53,13 @@ export const inspectAnalysisViewValidated = (
operation: parent.operation,
normalizedResult: parent.normalized_result,
limitations: parent.limitations,
+ artifact:
+ parent.subject === null
+ ? null
+ : {
+ path: parent.subject.local_path,
+ sha256: parent.subject.digest.sha256,
+ },
},
input.view,
);
@@ -126,7 +133,16 @@ const createAnalysisViewEvidence = (
confidence: "derived",
authority: parent.authority,
limitations: result.limitations,
- locations: [{ kind: "artifact-path", path: result.artifact.path }],
+ locations: [
+ ...(result.artifact.path.length > 0
+ ? [{ kind: "artifact-path" as const, path: result.artifact.path }]
+ : []),
+ ...(result.kind === "native" &&
+ result.procedure_address !== null &&
+ result.procedure_address.length > 0
+ ? [{ kind: "address" as const, address: result.procedure_address }]
+ : []),
+ ],
evidenceLinks: [parent.evidence_id],
});
};
diff --git a/src/cli/analysisViewCommands.ts b/src/cli/analysisViewCommands.ts
index a0deb02290..a193d27400 100644
--- a/src/cli/analysisViewCommands.ts
+++ b/src/cli/analysisViewCommands.ts
@@ -18,7 +18,7 @@ export const registerAnalysisViewCommands = (
): void => {
cli.command(CLI_COMMANDS.inspectAnalysisView, {
description:
- "Project a selected view of completed layout or JavaScript application Evidence JSON",
+ "Project a selected view of completed layout, JavaScript application or native function Evidence JSON",
args: z.object({
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
}),
diff --git a/src/contracts/analysisViewToolContracts.ts b/src/contracts/analysisViewToolContracts.ts
index b8f455767a..be65b229f2 100644
--- a/src/contracts/analysisViewToolContracts.ts
+++ b/src/contracts/analysisViewToolContracts.ts
@@ -8,14 +8,14 @@ import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js";
const RETAINED_EXAMPLE_ID = `ev_${"a".repeat(64)}`;
-/** Selected views of already completed layout and JavaScript analysis Evidence. */
+/** Selected views of already completed layout, JavaScript and native function Evidence. */
export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
{
name: "inspect_analysis_view",
...toolContractMetadata("inspect_analysis_view"),
kind: "application",
description:
- "Inspect a selected view of completed binary-layout or JavaScript application Evidence without repeating analysis. Use an exact same-session retained evidence_id or portable inline Evidence. Select a summary, layout mitigations/linkage facet, one section/symbol/module, or a stable page with a positive limit. Module pages contain JavaScript assets, bundled modules and source modules; use node_id when paths are ambiguous or unavailable. Returns selected facts inline with artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the retained Evidence when needed.",
+ "Inspect selected views of completed binary-layout, JavaScript application or native analyze_function Evidence without repeating analysis. Use an exact same-session retained evidence_id or portable inline Evidence. Select a summary, layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Native views select procedure, pseudocode, assembly, callers/callees, references and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.",
inputSchema: inspectAnalysisViewInputSchema,
outputSchema: evidenceResultOf(analysisViewResultSchema),
examples: [
@@ -43,6 +43,21 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
},
},
},
+ {
+ title: "Page native high-pcode operations from retained Evidence",
+ input: {
+ source: {
+ kind: "retained-evidence",
+ evidence_id: RETAINED_EXAMPLE_ID,
+ },
+ view: {
+ kind: "native",
+ facet: "value_flow_operations",
+ offset: 0,
+ limit: 32,
+ },
+ },
+ },
{
title: "Page module identities from retained application Evidence",
input: {
diff --git a/src/domain/analysisView/analysisView.test.ts b/src/domain/analysisView/analysisView.test.ts
index e3aabcf143..1fd6fb7f1a 100644
--- a/src/domain/analysisView/analysisView.test.ts
+++ b/src/domain/analysisView/analysisView.test.ts
@@ -1,6 +1,8 @@
import { expect, it } from "vitest";
import { resolveJavaScriptSourceMapReference } from "../javascript/javascriptSourceMapPaths.js";
+import { functionDossierSchema } from "../hopperValues.js";
+import { ghidraFunctionDossier } from "../ghidraValues.fixture.js";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
import {
analysisViewBindJavaScriptGraphs,
@@ -48,21 +50,190 @@ const javascriptParent = (
limitations: JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE.limitations,
});
-it("accepts caller-selected page sizes and rejects malformed bounds", () => {
- const input = {
- source: { kind: "retained-evidence", evidence_id: `ev_${"a".repeat(64)}` },
- view: { kind: "page", collection: "sections", offset: 0, limit: 500 },
- };
- expect(inspectAnalysisViewInputSchema.safeParse(input).success).toBe(true);
- for (const limit of [0, -1, 0.5, Infinity, Number.MAX_SAFE_INTEGER + 1])
- expect(
- inspectAnalysisViewInputSchema.safeParse({
- ...input,
- view: { ...input.view, limit },
- }).success,
- ).toBe(false);
+const nativeParent = (
+ result: unknown = ghidraFunctionDossier(),
+): AnalysisViewParent => ({
+ evidenceId: "ev_" + "b".repeat(64),
+ operation: "analyze_function",
+ normalizedResult: result,
+ limitations: ["Retained provider limitation"],
+ artifact: { path: "/fixtures/example.exe", sha256: "a".repeat(64) },
});
+it("projects native procedure and bounded assembly and high-pcode with exact parent identity", () => {
+ const parent = nativeParent();
+ const procedure = projectAnalysisView(parent, {
+ kind: "native",
+ facet: "procedure",
+ offset: 0,
+ limit: 64,
+ });
+ if (!procedure.ok) throw procedure.error;
+ expect(procedure.value).toMatchObject({
+ kind: "native",
+ parent_operation: "analyze_function",
+ artifact: parent.artifact,
+ item: { address: "0x401000" },
+ });
+ expect(procedure.value.view_digest).not.toBe(parent.evidenceId.slice(3));
+ const assembly = projectAnalysisView(parent, {
+ kind: "native",
+ facet: "assembly",
+ offset: 0,
+ limit: 1,
+ });
+ if (!assembly.ok) throw assembly.error;
+ expect(assembly.value).toMatchObject({
+ coverage: { examined: 1, total: 2, next_offset: 1 },
+ item: ["0x401000: CALL 0x401020"],
+ });
+ const flow = projectAnalysisView(parent, {
+ kind: "native",
+ facet: "value_flow_operations",
+ offset: 0,
+ limit: 1,
+ });
+ if (!flow.ok) throw flow.error;
+ expect(flow.value).toMatchObject({ item: [{ opcode: "COPY" }] });
+});
+
+it("rejects invalid native parents, unsupported source operations and singleton offsets", () => {
+ const view = {
+ kind: "native",
+ facet: "assembly",
+ offset: 0,
+ limit: 4,
+ } as const;
+ expect(projectAnalysisView(nativeParent({}), view)).toMatchObject({
+ ok: false,
+ error: { _tag: "AnalysisInputError" },
+ });
+ expect(
+ projectAnalysisView({ ...nativeParent(), artifact: null }, view),
+ ).toMatchObject({
+ ok: false,
+ error: { _tag: "AnalysisInputError" },
+ });
+ expect(projectAnalysisView(layoutParent(), view)).toMatchObject({
+ ok: false,
+ error: { _tag: "AnalysisInputError" },
+ });
+ expect(projectAnalysisView(javascriptParent(), view)).toMatchObject({
+ ok: false,
+ error: { _tag: "AnalysisInputError" },
+ });
+ expect(
+ projectAnalysisView(nativeParent(), {
+ kind: "native",
+ facet: "procedure",
+ offset: 1,
+ limit: 2,
+ }),
+ ).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
+});
+
+it("preserves native API limitations and unavailable residual unknowns", () => {
+ const original = functionDossierSchema.parse(ghidraFunctionDossier());
+ const view = {
+ kind: "native",
+ facet: "procedure",
+ offset: 0,
+ limit: 64,
+ } as const;
+ const unavailable = projectAnalysisView(
+ nativeParent({
+ ...original,
+ native_api: {
+ available: false,
+ reason: "No decompiler",
+ residual_unknowns: ["Calling convention unresolved"],
+ },
+ }),
+ view,
+ );
+ if (!unavailable.ok) throw unavailable.error;
+ expect(unavailable.value.unknowns).toContain("Calling convention unresolved");
+ const available = projectAnalysisView(nativeParent(), view);
+ if (!available.ok) throw available.error;
+ if (!original.native_api?.available) throw new Error("expected API fixture");
+ expect(available.value.limitations).toEqual(
+ expect.arrayContaining(original.native_api.limitations),
+ );
+});
+
+it("reports the malformed native dossier field", () => {
+ const result = projectAnalysisView(
+ nativeParent({
+ ...functionDossierSchema.parse(ghidraFunctionDossier()),
+ pseudocode: 12,
+ }),
+ {
+ kind: "native",
+ facet: "pseudocode",
+ offset: 0,
+ limit: 64,
+ },
+ );
+ expect(result).toMatchObject({
+ ok: false,
+ error: {
+ issues: expect.arrayContaining([
+ expect.objectContaining({
+ path: ["source", "normalized_result", "pseudocode"],
+ }),
+ ]),
+ },
+ });
+});
+
+it("native pseudocode pages do not split surrogate pairs", () => {
+ const original = functionDossierSchema.parse(ghidraFunctionDossier());
+ const parent = nativeParent({ ...original, pseudocode: "ab😀cd" });
+ expect(
+ projectAnalysisView(parent, {
+ kind: "native",
+ facet: "pseudocode",
+ offset: 3,
+ limit: 2,
+ }),
+ ).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
+ const safe = projectAnalysisView(parent, {
+ kind: "native",
+ facet: "pseudocode",
+ offset: 2,
+ limit: 3,
+ });
+ if (!safe.ok) throw safe.error;
+ expect(safe.value).toMatchObject({
+ item: { text: "😀c", unit: "utf16-code-units" },
+ coverage: { examined: 3, next_offset: 5 },
+ });
+});
+
+it.each([
+ { kind: "page", collection: "sections", offset: 0, limit: 500 },
+ { kind: "native", facet: "assembly", offset: 0, limit: 500 },
+])(
+ "accepts caller-selected page sizes and rejects malformed bounds: $kind",
+ (view) => {
+ const input = {
+ source: {
+ kind: "retained-evidence",
+ evidence_id: `ev_${"a".repeat(64)}`,
+ },
+ view,
+ };
+ expect(inspectAnalysisViewInputSchema.safeParse(input).success).toBe(true);
+ for (const limit of [0, -1, 0.5, Infinity, Number.MAX_SAFE_INTEGER + 1])
+ expect(
+ inspectAnalysisViewInputSchema.safeParse({
+ ...input,
+ view: { ...input.view, limit },
+ }).success,
+ ).toBe(false);
+ },
+);
+
it("projects layout summary, facet, item, and stable pages", () => {
const parent = layoutParent();
const layout = analysisViewLayoutFixture();
diff --git a/src/domain/analysisView/analysisView.ts b/src/domain/analysisView/analysisView.ts
index efb9f5d44b..db2d2dbac3 100644
--- a/src/domain/analysisView/analysisView.ts
+++ b/src/domain/analysisView/analysisView.ts
@@ -12,6 +12,7 @@ import { jsonObjectSchema, jsonValueSchema } from "../jsonValue.js";
import { err, ok, type Result } from "../result.js";
import type { AnalysisError } from "../analysisErrorBase.js";
import { binaryLayoutSchema } from "../native/binaryLayout.js";
+import { functionDossierSchema } from "../hopperValues.js";
import {
parseOwnedJavaScriptApplicationAnalysisSteps,
type JavaScriptApplicationAnalysisResult,
@@ -19,6 +20,7 @@ import {
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
import { projectBinaryLayoutView } from "./binaryLayoutView.js";
import { projectJavaScriptApplicationView } from "./javascriptApplicationView.js";
+import { projectNativeFunctionView } from "./nativeFunctionView.js";
/** Public name of the selected-view workflow. */
export const INSPECT_ANALYSIS_VIEW_OPERATION = "inspect_analysis_view" as const;
@@ -73,12 +75,35 @@ const pageViewSchema = z.strictObject({
limit: z.number().int().positive(),
});
+/** Bounded native function facets of already authenticated analyze_function Evidence. */
+const nativeViewSchema = z.strictObject({
+ kind: z.literal("native"),
+ facet: z.enum([
+ "procedure",
+ "pseudocode",
+ "assembly",
+ "callers",
+ "callees",
+ "incoming_references",
+ "outgoing_references",
+ "value_flow_summary",
+ "value_flow_operations",
+ "value_flow_def_use",
+ "value_flow_effects",
+ "value_flow_parameters",
+ "value_flow_parameter_uses",
+ ]),
+ offset: z.number().int().nonnegative().default(0),
+ limit: z.number().int().positive().default(64),
+});
+
/** Caller-selected projection of already completed analysis Evidence. */
export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
summaryViewSchema,
facetViewSchema,
itemViewSchema,
pageViewSchema,
+ nativeViewSchema,
]);
/** Inline Evidence or an exact same-session retained reference. */
@@ -108,7 +133,9 @@ const coverageSchema = z.strictObject({
});
const artifactSchema = z.strictObject({
- path: z.string().min(1),
+ path: z
+ .string()
+ .describe("Exact recorded artifact path, including an empty string."),
sha256: digestSchema,
});
@@ -150,6 +177,7 @@ const viewResultBase = {
parent_operation: z.enum([
"inspect_binary_layout",
"analyze_javascript_application",
+ "analyze_function",
]),
parent_digest: digestSchema,
view_digest: digestSchema,
@@ -185,6 +213,13 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
items: z.array(jsonValueSchema),
...viewResultBase,
}),
+ z.strictObject({
+ kind: z.literal("native"),
+ view: nativeViewSchema,
+ procedure_address: z.string().nullable(),
+ item: jsonValueSchema,
+ ...viewResultBase,
+ }),
]);
export type AnalysisViewRequest = z.output;
@@ -200,6 +235,7 @@ export interface AnalysisViewParent {
readonly operation: string;
readonly normalizedResult: unknown;
readonly limitations: readonly string[];
+ readonly artifact?: { readonly path: string; readonly sha256: string } | null;
}
/** Construct a typed input failure for one selected-view constraint. */
@@ -216,7 +252,11 @@ export type UnsignedAnalysisView =
>
| Omit, "view_digest">
| Omit, "view_digest">
- | Omit, "view_digest">;
+ | Omit, "view_digest">
+ | Omit<
+ Extract,
+ "view_digest"
+ >;
/** Attach the content digest that must change when projected bytes change. */
export const sealAnalysisView = (
@@ -261,7 +301,7 @@ const unsupportedParent = (
new AnalysisUnsupportedTargetError(
INSPECT_ANALYSIS_VIEW_OPERATION,
parentArtifactPath(parent),
- `Parent Evidence operation ${JSON.stringify(parent.operation)} is not inspect_binary_layout or analyze_javascript_application.`,
+ `Parent Evidence operation ${JSON.stringify(parent.operation)} is not inspect_binary_layout, analyze_javascript_application or analyze_function.`,
);
const parentArtifactPath = (parent: AnalysisViewParent): string => {
@@ -327,5 +367,22 @@ export const projectAnalysisView = (
);
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
}
+ if (parent.operation === "analyze_function") {
+ const native = functionDossierSchema.safeParse(parent.normalizedResult);
+ if (!native.success)
+ return err(
+ analysisInputErrorFromIssues(
+ INSPECT_ANALYSIS_VIEW_OPERATION,
+ native.error.issues.map((issue) => ({
+ ...issue,
+ path: ["source", "normalized_result", ...issue.path],
+ })),
+ { source: { normalized_result: parent.normalizedResult } },
+ { cause: native.error },
+ ),
+ );
+ const projected = projectNativeFunctionView(parent, native.data, view);
+ return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
+ }
return err(unsupportedParent(parent));
};
diff --git a/src/domain/analysisView/binaryLayoutView.ts b/src/domain/analysisView/binaryLayoutView.ts
index 5a52904a85..13a9b766f3 100644
--- a/src/domain/analysisView/binaryLayoutView.ts
+++ b/src/domain/analysisView/binaryLayoutView.ts
@@ -152,6 +152,10 @@ export const projectBinaryLayoutView = (
view: AnalysisViewRequest,
): Result => {
const shared = parentFields(parent, layout);
+ if (view.kind === "native")
+ return err(
+ layoutIncompatible("Native views require analyze_function Evidence."),
+ );
if (view.kind === "summary")
return ok({
kind: "summary",
diff --git a/src/domain/analysisView/javascriptApplicationView.ts b/src/domain/analysisView/javascriptApplicationView.ts
index 5216c72b44..d6e994fc94 100644
--- a/src/domain/analysisView/javascriptApplicationView.ts
+++ b/src/domain/analysisView/javascriptApplicationView.ts
@@ -243,6 +243,10 @@ export const projectJavaScriptApplicationView = (
): Result => {
const shared = parentFields(parent, analysis);
const modules = moduleEntries(analysis.graph.nodes);
+ if (view.kind === "native")
+ return err(
+ jsIncompatible("Native views require analyze_function Evidence."),
+ );
if (view.kind === "summary")
return ok({
kind: "summary",
diff --git a/src/domain/analysisView/nativeFunctionView.ts b/src/domain/analysisView/nativeFunctionView.ts
new file mode 100644
index 0000000000..d17f69b038
--- /dev/null
+++ b/src/domain/analysisView/nativeFunctionView.ts
@@ -0,0 +1,202 @@
+import type { AnalysisError } from "../analysisErrorBase.js";
+import { uniqueSorted } from "../canonicalOrdering.js";
+import type { FunctionDossier } from "../hopperValues.js";
+import { jsonValueSchema } from "../jsonValue.js";
+import { err, ok, type Result } from "../result.js";
+import {
+ analysisViewInputError,
+ type AnalysisViewCoverage,
+ type AnalysisViewParent,
+ type AnalysisViewRequest,
+ type UnsignedAnalysisView,
+} from "./analysisView.js";
+import {
+ completeWithinViewCoverage,
+ pageViewCoverage,
+} from "./analysisViewCoverage.js";
+
+type Artifact = { readonly path: string; readonly sha256: string };
+
+const viewError = (message: string): AnalysisError =>
+ analysisViewInputError([
+ { path: ["view"], reason: "invalid_value", message },
+ ]);
+
+const parentFields = (
+ parent: AnalysisViewParent,
+ dossier: FunctionDossier,
+ artifact: Artifact,
+) => {
+ const flow = dossier.native_value_flow;
+ const api = dossier.native_api;
+ const unknowns: string[] = [];
+ if (flow === null) unknowns.push("native_value_flow was not recorded");
+ else if (!flow.available)
+ unknowns.push("native_value_flow unavailable: " + flow.reason);
+ else if (flow.truncated)
+ unknowns.push(
+ "native_value_flow is truncated; omitted observations remain unknown",
+ );
+ if (api === null) unknowns.push("native_api was not recorded");
+ else if (!api.available)
+ unknowns.push(
+ "native_api unavailable: " + api.reason,
+ ...api.residual_unknowns,
+ );
+ return {
+ parent_evidence_id: parent.evidenceId,
+ parent_operation: "analyze_function" as const,
+ parent_digest: parent.evidenceId.slice(3),
+ procedure_address: dossier.procedure.address,
+ artifact,
+ limitations: uniqueSorted([
+ ...parent.limitations,
+ ...dossier.limitations,
+ ...(flow?.limitations ?? []),
+ ...(api?.available ? api.limitations : []),
+ ]),
+ unknowns: uniqueSorted(unknowns),
+ };
+};
+
+const pseudocodePage = (
+ body: string,
+ offset: number,
+ limit: number,
+): Result<
+ {
+ readonly item: { readonly text: string; readonly unit: "utf16-code-units" };
+ readonly coverage: AnalysisViewCoverage;
+ },
+ AnalysisError
+> => {
+ const start = Math.min(offset, body.length);
+ const high = (n: number) => n >= 0xd800 && n <= 0xdbff;
+ const low = (n: number) => n >= 0xdc00 && n <= 0xdfff;
+ if (
+ start > 0 &&
+ start < body.length &&
+ high(body.charCodeAt(start - 1)) &&
+ low(body.charCodeAt(start))
+ )
+ return err(viewError("Pseudocode offset splits a UTF-16 surrogate pair."));
+ let end = Math.min(start + limit, body.length);
+ if (
+ end > start &&
+ end < body.length &&
+ high(body.charCodeAt(end - 1)) &&
+ low(body.charCodeAt(end))
+ )
+ end--;
+ if (end === start && start < body.length)
+ return err(
+ viewError(
+ "Increase pseudocode limit to include the next complete Unicode character.",
+ ),
+ );
+ return ok({
+ item: { text: body.slice(start, end), unit: "utf16-code-units" },
+ coverage: pageViewCoverage(start, end - start, body.length),
+ });
+};
+
+const nativeRows = (
+ dossier: FunctionDossier,
+ facet: Exclude<
+ Extract["facet"],
+ "procedure" | "pseudocode" | "value_flow_summary"
+ >,
+) => {
+ const sourceFlow = dossier.native_value_flow;
+ const flow = sourceFlow?.available ? sourceFlow : undefined;
+ return {
+ assembly: dossier.assembly,
+ callers: dossier.callers,
+ callees: dossier.callees,
+ incoming_references: dossier.incoming_references,
+ outgoing_references: dossier.outgoing_references,
+ value_flow_operations: flow?.operations,
+ value_flow_def_use: flow?.def_use,
+ value_flow_effects: flow?.effects,
+ value_flow_parameters: flow?.parameters,
+ value_flow_parameter_uses: flow?.parameter_uses,
+ }[facet];
+};
+
+/** Project bounded native function facets from complete retained Evidence without reanalysis. */
+export const projectNativeFunctionView = (
+ parent: AnalysisViewParent,
+ dossier: FunctionDossier,
+ view: AnalysisViewRequest,
+): Result => {
+ if (view.kind !== "native")
+ return err(viewError("analyze_function Evidence requires a native view."));
+ const artifact = parent.artifact;
+ if (!artifact)
+ return err(
+ analysisViewInputError([
+ {
+ path: ["source", "subject"],
+ reason: "invalid_value",
+ message:
+ "Native function Evidence requires an artifact-bound subject.",
+ },
+ ]),
+ );
+ const common = parentFields(parent, dossier, artifact);
+ const singleton = (item: unknown): UnsignedAnalysisView => ({
+ kind: "native",
+ view,
+ item: jsonValueSchema.parse(item),
+ coverage: completeWithinViewCoverage(1, 1),
+ ...common,
+ });
+ const flow = dossier.native_value_flow;
+ if (view.facet === "procedure" || view.facet === "value_flow_summary") {
+ if (view.offset !== 0)
+ return err(viewError("Singleton native facets require offset 0."));
+ if (view.facet === "procedure") return ok(singleton(dossier.procedure));
+ const status =
+ flow === null
+ ? { available: false, reason: "not recorded" }
+ : !flow.available
+ ? { available: false, reason: flow.reason }
+ : {
+ available: true,
+ provenance: flow.provenance,
+ truncated: flow.truncated,
+ omitted_operations_lower_bound:
+ flow.omitted_operations_lower_bound,
+ known_omitted_inputs: flow.known_omitted_inputs,
+ known_omitted_edges: flow.known_omitted_edges,
+ operations: flow.operations.length,
+ def_use: flow.def_use.length,
+ effects: flow.effects.length,
+ parameters: flow.parameters.length,
+ parameter_uses: flow.parameter_uses.length,
+ };
+ return ok(singleton(status));
+ }
+ if (view.facet === "pseudocode") {
+ const page = pseudocodePage(dossier.pseudocode, view.offset, view.limit);
+ return page.ok
+ ? ok({ kind: "native", view, ...page.value, ...common })
+ : page;
+ }
+ const rows = nativeRows(dossier, view.facet);
+ if (rows === undefined)
+ return ok(
+ singleton({
+ available: false,
+ reason: flow?.available === false ? flow.reason : "not recorded",
+ }),
+ );
+ const items = rows.slice(view.offset, view.offset + view.limit);
+ return ok({
+ kind: "native",
+ view,
+ item: jsonValueSchema.parse(items),
+ coverage: pageViewCoverage(view.offset, items.length, rows.length),
+ ...common,
+ });
+};
diff --git a/tests/boundary/cli/analysisViewCli.test.ts b/tests/boundary/cli/analysisViewCli.test.ts
index 86e0d9b271..2abaf700ac 100644
--- a/tests/boundary/cli/analysisViewCli.test.ts
+++ b/tests/boundary/cli/analysisViewCli.test.ts
@@ -7,6 +7,8 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analysisViewLayoutEvidence } from "../../fixtures/analysisView.js";
+import { createEvidence } from "../../../src/domain/evidence.js";
+import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
cliTest(
"projects one section from a JSON file of inline layout Evidence",
async ({ cli }) => {
@@ -43,6 +45,48 @@ cliTest(
},
);
+cliTest(
+ "projects native function pseudocode without requiring Ghidra",
+ async ({ cli }) => {
+ const root = await createTestTempDirectory("rea-native-view-cli-");
+ const parent = createEvidence(
+ { path: "/fixtures/native.exe", format: "pe", sha256: "a".repeat(64) },
+ { id: "ghidra", name: "Ghidra", version: "12.1.4" },
+ {
+ operation: "analyze_function",
+ parameters: { address: "0x401000" },
+ result: ghidraFunctionDossier(),
+ },
+ );
+ const input = join(root, "native-view.json");
+ await writeFile(
+ input,
+ JSON.stringify({
+ source: { kind: "inline", evidence: parent },
+ view: { kind: "native", facet: "pseudocode", offset: 0, limit: 12 },
+ }),
+ );
+ const result = await cli.run({
+ arguments: ["inspect-analysis-view", input, "--json"],
+ environment: { HOME: root, XDG_CONFIG_HOME: root, XDG_CACHE_HOME: root },
+ });
+ expect(result.exitCode).toBe(0);
+ expect(result.json).toMatchObject({
+ operation: "inspect_analysis_view",
+ normalized_result: {
+ kind: "native",
+ parent_evidence_id: parent.evidence_id,
+ procedure_address: "0x401000",
+ item: { text: "int fixture_", unit: "utf16-code-units" },
+ },
+ locations: [
+ { kind: "artifact-path", path: "/fixtures/native.exe" },
+ { kind: "address", address: "0x401000" },
+ ],
+ });
+ },
+);
+
cliTest("rejects malformed JSON before projection", async ({ cli }) => {
const root = await createTestTempDirectory("rea-analysis-view-cli-bad-");
const result = await cli.run({
diff --git a/tests/boundary/mcp/analysisViewMcp.test.ts b/tests/boundary/mcp/analysisViewMcp.test.ts
index 3edeffba78..3c15312aa6 100644
--- a/tests/boundary/mcp/analysisViewMcp.test.ts
+++ b/tests/boundary/mcp/analysisViewMcp.test.ts
@@ -6,7 +6,9 @@ import { z } from "zod";
import { BinaryLayoutService } from "../../../src/application/binaryDiagnostics/BinaryLayoutService.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
-import { parseEvidence } from "../../../src/domain/evidence.js";
+import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
+import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
+import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
import { ok } from "../../../src/domain/result.js";
import {
createJavaScriptApplicationGraph,
@@ -109,6 +111,68 @@ it("advertises exact schemas and projects one layout section from retained Evide
);
});
+it("delivers a bounded native dossier view over MCP without a Ghidra provider or lost connection", async () => {
+ const { client, session } = await connect();
+ const original = functionDossierSchema.parse(ghidraFunctionDossier());
+ const parent = createEvidence(
+ {
+ path: "/fixtures/oversized-native.exe",
+ format: "pe",
+ sha256: "b".repeat(64),
+ },
+ { id: "ghidra", name: "Ghidra", version: "12.1.4" },
+ {
+ operation: "analyze_function",
+ parameters: { address: "0x401000" },
+ result: { ...original, pseudocode: "X".repeat(11 * 1024 * 1024) },
+ },
+ );
+ expect(session.recordEvidence(parent).ok).toBe(true);
+ const advertised = (await client.listTools()).tools.find(
+ (tool) => tool.name === "inspect_analysis_view",
+ );
+ if (advertised?.outputSchema === undefined)
+ throw new Error("missing view schemas");
+ const ajv = new Ajv2020({ strict: false, validateFormats: false });
+ const inputSchema: Record = advertised.inputSchema;
+ const outputSchema: Record = advertised.outputSchema;
+ const arguments_ = {
+ source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
+ view: { kind: "native", facet: "pseudocode", offset: 0, limit: 500 },
+ };
+ expect(ajv.validateSchema(inputSchema)).toBe(true);
+ expect(ajv.validateSchema(outputSchema)).toBe(true);
+ expect(
+ ajv.validate(inputSchema, arguments_),
+ JSON.stringify(ajv.errors),
+ ).toBe(true);
+ const response = await client.callTool({
+ name: "inspect_analysis_view",
+ arguments: arguments_,
+ });
+ expect(response.isError).not.toBe(true);
+ expect(
+ ajv.validate(outputSchema, response.structuredContent),
+ JSON.stringify(ajv.errors),
+ ).toBe(true);
+ const parsed = toolContract("inspect_analysis_view").outputSchema.parse(
+ response.structuredContent,
+ );
+ expect(parsed.normalized_result).toMatchObject({
+ kind: "native",
+ parent_evidence_id: parent.evidence_id,
+ procedure_address: "0x401000",
+ coverage: { examined: 500, next_offset: 500, total: 11 * 1024 * 1024 },
+ });
+ expect(parsed.evidence_links).toEqual([parent.evidence_id]);
+ expect(parsed.locations).toEqual([
+ { kind: "artifact-path", path: "/fixtures/oversized-native.exe" },
+ { kind: "address", address: "0x401000" },
+ ]);
+ expect(JSON.stringify(response.structuredContent).length).toBeLessThan(12000);
+ await client.ping();
+});
+
it("retains an oversized selected observation and keeps subsequent views usable", async () => {
const { client, session } = await connect();
const analysis = analysisViewJavaScriptAnalysisWithSource();
@@ -185,6 +249,71 @@ it("retains an oversized selected observation and keeps subsequent views usable"
expect(summary.isError).not.toBe(true);
});
+it("retains an oversized native assembly row and permits a subsequent smaller view", async () => {
+ const { client, session } = await connect();
+ const row = "\0".repeat(1_000_000);
+ const parent = createEvidence(
+ { path: "/fixtures/large-row.exe", format: "pe", sha256: "c".repeat(64) },
+ { id: "ghidra", name: "Ghidra", version: "12.1.4" },
+ {
+ operation: "analyze_function",
+ parameters: {},
+ result: {
+ ...functionDossierSchema.parse(ghidraFunctionDossier()),
+ assembly: [row],
+ },
+ },
+ );
+ expect(session.recordEvidence(parent).ok).toBe(true);
+ const source = { kind: "retained-evidence", evidence_id: parent.evidence_id };
+ const response = await client.callTool({
+ name: "inspect_analysis_view",
+ arguments: {
+ source,
+ view: { kind: "native", facet: "assembly", offset: 0, limit: 1 },
+ },
+ });
+ expect(response.isError).toBe(true);
+ const error = z
+ .object({
+ error: z.object({
+ details: z.object({
+ reported_limits: z.object({
+ evidence_reference: z.object({
+ kind: z.literal("retained-evidence"),
+ evidence_id: z.string(),
+ }),
+ }),
+ }),
+ }),
+ })
+ .parse(parseMcpToolError(response));
+ const retained = session.evidenceById(
+ error.error.details.reported_limits.evidence_reference.evidence_id,
+ );
+ expect(retained?.normalized_result).toMatchObject({
+ item: [row],
+ parent_evidence_id: parent.evidence_id,
+ });
+ const next = await client.callTool({
+ name: "inspect_analysis_view",
+ arguments: {
+ source,
+ view: { kind: "native", facet: "pseudocode", offset: 0, limit: 12 },
+ },
+ });
+ expect(next.isError).not.toBe(true);
+ expect(
+ toolContract("inspect_analysis_view").outputSchema.parse(
+ next.structuredContent,
+ ).normalized_result,
+ ).toMatchObject({
+ item: { text: "int fixture_" },
+ parent_evidence_id: parent.evidence_id,
+ });
+ await client.ping();
+});
+
it("returns a JavaScript summary without graph payloads from a retained reference", async () => {
const { client, session } = await connect();
const parent = analysisViewJavaScriptEvidence();
From f4a0c1ce95375c1661ca95b414829113a10298b8 Mon Sep 17 00:00:00 2001
From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com>
Date: Sat, 10 Oct 2026 12:20:05 +0800
Subject: [PATCH 17/74] fix(javascript)!: avoid event payload copies in graph
metadata (#1427)
Share the canonical string identity rule with endpoint projection, scope event keys to each emitter, and keep complete event/listener names in properties. Verify real CLI and stdio MCP with large names, registration/removal, distinct emitters, exact name seeds, reserved-prefix values, and unresolved dynamic names.
BREAKING CHANGE: New analyses use digest-based IDs for long or reserved-prefix event names and shorter event/listener display labels. Select IDs from the parent graph and read exact names from event_name properties. Stored graphs and their original IDs remain accepted for semantic tracing.
---
docs/javascript-application-workflows.md | 10 ++
.../JavaScriptArtifactGraphFindings.ts | 11 +-
.../JavaScriptSemanticGraphAsyncProjection.ts | 13 +-
src/domain/compactStringIdentity.ts | 10 ++
.../javascriptEventPayload.test.ts | 164 ++++++++++++++++++
5 files changed, 196 insertions(+), 12 deletions(-)
create mode 100644 src/domain/compactStringIdentity.ts
create mode 100644 tests/acceptance/applications/javascriptEventPayload.test.ts
diff --git a/docs/javascript-application-workflows.md b/docs/javascript-application-workflows.md
index 592276e15e..f26c16d1f6 100644
--- a/docs/javascript-application-workflows.md
+++ b/docs/javascript-application-workflows.md
@@ -118,6 +118,16 @@ Use IDs from the selected graph and read the complete endpoint properties;
previously stored graphs still support their original IDs. Literal endpoint
search, semantic tracing, and version comparison use the preserved values.
+Event and listener nodes likewise preserve the complete event name in
+`properties.event_name`. Event role keys use the same reserved-prefix string
+digest rule, scoped to the emitter; repeated registrations, removals, and
+dispatches keep one event per exact emitter/name pair. Long event labels use
+`event`, and listener labels use the shorter of `:` and
+`:listener`. Dynamic names remain `null` with explicit uncertainty. New analyses
+change hashed event IDs and longer event/listener labels; query the preserved
+name or select IDs from the parent graph. Previously stored graphs keep their
+original IDs and remain valid trace inputs.
+
## Version comparison
REA pairs entities only when a tier produces one unique candidate on each side.
diff --git a/src/application/javascript/JavaScriptArtifactGraphFindings.ts b/src/application/javascript/JavaScriptArtifactGraphFindings.ts
index 580a4d0355..41f480daf3 100644
--- a/src/application/javascript/JavaScriptArtifactGraphFindings.ts
+++ b/src/application/javascript/JavaScriptArtifactGraphFindings.ts
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
-import { digestCanonicalValue } from "../../domain/canonicalDigest.js";
+import { compactStringIdentityKey } from "../../domain/compactStringIdentity.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
@@ -163,14 +163,7 @@ const addEndpoint = (
const source =
sourceNodeFor(context, input.file.path, input.value.module_key) ??
input.asset;
- const digestPrefix = "value-sha256:";
- const valueKey = `${digestPrefix}${digestCanonicalValue(input.value.value, "JavaScript endpoint")}`;
- // Reserve the digest prefix so an exact caller value cannot alias a digest key.
- const key =
- input.value.value.length <= valueKey.length &&
- !input.value.value.startsWith(digestPrefix)
- ? input.value.value
- : valueKey;
+ const key = compactStringIdentityKey(input.value.value);
const label = `${input.value.kind} endpoint`;
const node = context.accumulator.addNode({
kind: "endpoint",
diff --git a/src/application/javascript/JavaScriptSemanticGraphAsyncProjection.ts b/src/application/javascript/JavaScriptSemanticGraphAsyncProjection.ts
index a8814281f3..f54c5c4f31 100644
--- a/src/application/javascript/JavaScriptSemanticGraphAsyncProjection.ts
+++ b/src/application/javascript/JavaScriptSemanticGraphAsyncProjection.ts
@@ -1,3 +1,4 @@
+import { compactStringIdentityKey } from "../../domain/compactStringIdentity.js";
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type {
@@ -21,7 +22,7 @@ export const projectSemanticEvents = (
const eventKey =
operation.eventName === null
? operation.eventId
- : `${operation.emitterKey}\u0000${operation.eventName}`;
+ : `${operation.emitterKey}\u0000${compactStringIdentityKey(operation.eventName)}`;
const event =
eventNodes.get(eventKey) ?? addEventNode(context, operation, eventKey);
if (event === null) continue;
@@ -59,7 +60,11 @@ const addEventNode = (
kind: "event",
roleKey: `event:${eventKey}`,
location: operation.eventName === null ? operation.location : null,
- label: operation.eventName,
+ label:
+ operation.eventName === null ||
+ operation.eventName.length <= "event".length
+ ? operation.eventName
+ : "event",
functionNodeId: null,
properties: {
emitter_key: operation.emitterKey,
@@ -78,7 +83,9 @@ const addListenerNode = (
label:
operation.eventName === null
? operation.method
- : `${operation.method}:${operation.eventName}`,
+ : operation.eventName.length <= "listener".length
+ ? `${operation.method}:${operation.eventName}`
+ : `${operation.method}:listener`,
functionNodeId:
operation.ownerCallableId === null
? null
diff --git a/src/domain/compactStringIdentity.ts b/src/domain/compactStringIdentity.ts
new file mode 100644
index 0000000000..421555a908
--- /dev/null
+++ b/src/domain/compactStringIdentity.ts
@@ -0,0 +1,10 @@
+import { digestCanonicalValue } from "./canonicalDigest.js";
+
+/** Retain short exact string keys or commit their value in a reserved digest namespace. */
+export const compactStringIdentityKey = (value: string): string => {
+ const prefix = "value-sha256:";
+ const digestKey = `${prefix}${digestCanonicalValue(value, "String identity")}`;
+ return value.length <= digestKey.length && !value.startsWith(prefix)
+ ? value
+ : digestKey;
+};
diff --git a/tests/acceptance/applications/javascriptEventPayload.test.ts b/tests/acceptance/applications/javascriptEventPayload.test.ts
new file mode 100644
index 0000000000..c4689250de
--- /dev/null
+++ b/tests/acceptance/applications/javascriptEventPayload.test.ts
@@ -0,0 +1,164 @@
+import { Client } from "@modelcontextprotocol/client";
+import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
+import { createHash } from "node:crypto";
+import { writeFile } from "node:fs/promises";
+import { join, resolve } from "node:path";
+import { expect } from "vitest";
+
+import { toolContract } from "../../../src/contracts/toolContracts.js";
+import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
+import { javaScriptSemanticTraceResultSchema } from "../../../src/domain/javascript/javascriptSemanticTraceSchemas.js";
+import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
+import { cliTest } from "../../support/cli/cliFixture.js";
+
+cliTest(
+ "retains large event names and emitter-scoped listener ownership through CLI and stdio MCP",
+ async ({ cli }) => {
+ const root = await createTestTempDirectory("rea-event-payload-");
+ const name = "payload/ğ".repeat(80_000);
+ const lookalike = `value-sha256:${createHash("sha256").update(JSON.stringify(name)).digest("hex")}`;
+ const source = [
+ 'import {EventEmitter} from "node:events";',
+ "const bus = new EventEmitter(); const other = new EventEmitter(); const handler = () => {};",
+ `bus.on(${JSON.stringify(name)}, handler);`,
+ `bus.emit(${JSON.stringify(name)});`,
+ `bus.off(${JSON.stringify(name)}, handler);`,
+ `bus.on(${JSON.stringify(lookalike)}, handler); bus.emit(${JSON.stringify(lookalike)});`,
+ 'bus.on("ok", handler); bus.emit("ok"); other.on("ok", handler); other.emit("ok");',
+ "bus.on(getEventName(), handler);",
+ ].join("\n");
+ await writeFile(join(root, "main.js"), source);
+ const cliResponse = await cli.run({
+ arguments: [
+ "analyze-javascript-application",
+ root,
+ "--artifact-format",
+ "directory",
+ "--json",
+ ],
+ timeoutMs: 60_000,
+ });
+ expect(cliResponse.exitCode).toBe(0);
+ const cliEvidence = toolContract(
+ "analyze_javascript_application",
+ ).outputSchema.parse(cliResponse.json);
+ const client = new Client({ name: "event-payload-e2e", version: "1" });
+ const transport = new StdioClientTransport({
+ command: process.execPath,
+ args: [resolve("scripts/rea.mjs"), "mcp"],
+ cwd: process.cwd(),
+ env: { PATH: process.env.PATH ?? "" },
+ stderr: "pipe",
+ });
+ transport.stderr?.on("data", () => undefined);
+ try {
+ await client.connect(transport);
+ const response = await client.callTool({
+ name: "analyze_javascript_application",
+ arguments: { input_path: root, format: "directory" },
+ });
+ expect(response.isError).not.toBe(true);
+ const evidence = toolContract(
+ "analyze_javascript_application",
+ ).outputSchema.parse(response.structuredContent);
+ expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
+ const text = response.content.find((item) => item.type === "text");
+ if (text?.type !== "text") throw new Error("Missing MCP text result");
+ expect(
+ toolContract("analyze_javascript_application").outputSchema.parse(
+ JSON.parse(text.text),
+ ).evidence_id,
+ ).toBe(evidence.evidence_id);
+ const graph = javascriptApplicationAnalysisResultSchema.parse(
+ evidence.normalized_result,
+ ).semantic_graph;
+ const events = graph.nodes.filter((node) => node.kind === "event");
+ const selected = events.filter(
+ (node) => node.properties.event_name === name,
+ );
+ expect(selected).toHaveLength(1);
+ const event = selected[0];
+ if (event === undefined) throw new Error("Complete event was lost");
+ const listeners = graph.nodes.filter(
+ (node) =>
+ node.kind === "listener" && node.properties.event_name === name,
+ );
+ expect(new Set(listeners.map((node) => node.properties.method))).toEqual(
+ new Set(["on", "off"]),
+ );
+ const listenerIds = new Set(listeners.map((node) => node.node_id));
+ expect(
+ graph.relations
+ .filter(
+ (relation) =>
+ relation.source_node_id === event.node_id &&
+ listenerIds.has(relation.target_node_id),
+ )
+ .map((relation) => relation.relation),
+ ).toEqual(
+ expect.arrayContaining(["registers-listener", "removes-listener"]),
+ );
+ const lookalikeEvent = events.find(
+ (node) => node.properties.event_name === lookalike,
+ );
+ expect(lookalikeEvent).toBeDefined();
+ expect(lookalikeEvent?.node_id).not.toBe(event.node_id);
+ const shortEvents = events.filter(
+ (node) => node.properties.event_name === "ok",
+ );
+ expect(shortEvents).toHaveLength(2);
+ expect(
+ new Set(shortEvents.map((node) => node.properties.emitter_key)).size,
+ ).toBe(2);
+ expect(
+ shortEvents.every(
+ (node) =>
+ node.label === "ok" && node.identity.role_key.endsWith("\0ok"),
+ ),
+ ).toBe(true);
+ const dynamic = events.find(
+ (node) => node.properties.event_name === null,
+ );
+ expect(dynamic?.label).toBeNull();
+ expect(
+ graph.unknowns.some(
+ (unknown) =>
+ unknown.node_id === dynamic?.node_id && unknown.family === "event",
+ ),
+ ).toBe(true);
+ const traceResponse = await client.callTool({
+ name: "trace_javascript_semantics",
+ arguments: {
+ application: {
+ kind: "retained-evidence",
+ evidence_id: evidence.evidence_id,
+ },
+ query: {
+ seed: { kind: "event", name },
+ direction: "forward-influence",
+ },
+ },
+ });
+ expect(traceResponse.isError).not.toBe(true);
+ const traceEvidence = toolContract(
+ "trace_javascript_semantics",
+ ).outputSchema.parse(traceResponse.structuredContent);
+ const trace = javaScriptSemanticTraceResultSchema.parse(
+ traceEvidence.normalized_result,
+ );
+ expect(new Set(trace.seed_node_ids)).toEqual(
+ new Set([event.node_id, ...listenerIds]),
+ );
+ expect(
+ trace.nodes
+ .filter((node) => listenerIds.has(node.node_id))
+ .map((node) => node.properties.event_name),
+ ).toEqual(listeners.map(() => name));
+ await client.ping();
+ } finally {
+ await client.close();
+ await transport.close();
+ }
+ },
+ 120_000,
+);
From c29931ce81dc530d2623ec992714a70b1f3e50e4 Mon Sep 17 00:00:00 2001
From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com>
Date: Sat, 10 Oct 2026 00:25:00 -0400
Subject: [PATCH 18/74] fix(javascript): scan direct returns without recursive
stack growth
Use the shared AST child enumeration with an explicit stack, preserving return order and nested callable boundaries.
Refs #1065.
Co-authored-by: rudycelekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
---
.../javascript/javascriptSemanticReturns.ts | 14 +++---
.../javascriptApplicationDeepReturns.test.ts | 44 +++++++++++++++++++
2 files changed, 52 insertions(+), 6 deletions(-)
create mode 100644 tests/boundary/filesystem/javascriptApplicationDeepReturns.test.ts
diff --git a/src/domain/javascript/javascriptSemanticReturns.ts b/src/domain/javascript/javascriptSemanticReturns.ts
index bb931fba51..44cbbea5bb 100644
--- a/src/domain/javascript/javascriptSemanticReturns.ts
+++ b/src/domain/javascript/javascriptSemanticReturns.ts
@@ -131,15 +131,17 @@ const directReturnExpressions = (callable: t.Node): ReturnExpression[] => {
return [{ node: callable.body, location: range(callable.body) }];
if (!t.isFunction(callable)) return [];
const output: ReturnExpression[] = [];
- const visit = (node: t.Node): void => {
- if (t.isFunction(node) || t.isClass(node)) return;
+ const pending = childNodes(callable.body).reverse();
+ while (pending.length > 0) {
+ const node = pending.pop();
+ if (node === undefined || t.isFunction(node) || t.isClass(node)) continue;
if (t.isReturnStatement(node)) {
output.push({ node: node.argument ?? null, location: range(node) });
- return;
+ continue;
}
- for (const child of childNodes(node)) visit(child);
- };
- for (const child of childNodes(callable.body)) visit(child);
+ // Reverse children so the explicit stack retains source-tree order.
+ for (const child of childNodes(node).reverse()) pending.push(child);
+ }
return output;
};
diff --git a/tests/boundary/filesystem/javascriptApplicationDeepReturns.test.ts b/tests/boundary/filesystem/javascriptApplicationDeepReturns.test.ts
new file mode 100644
index 0000000000..32a2d99af7
--- /dev/null
+++ b/tests/boundary/filesystem/javascriptApplicationDeepReturns.test.ts
@@ -0,0 +1,44 @@
+import { execFile } from "node:child_process";
+import { writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { promisify } from "node:util";
+
+import { expect, it } from "vitest";
+
+import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
+
+it("analyzes direct returns after deep source without exhausting the default process stack", async () => {
+ const root = await createTestTempDirectory("rea-deep-direct-returns-");
+ await writeFile(
+ join(root, "app.js"),
+ `export function render() { object${".property".repeat(20_000)}; function nested() { return "nested"; } class Inner { method() { return "method"; } } return "ready"; }`,
+ );
+ const service = new URL(
+ "../../../dist/application/javascript/JavaScriptApplicationService.js",
+ import.meta.url,
+ ).href;
+ const script = `
+ import { analyzeJavaScriptApplication } from ${JSON.stringify(service)};
+ const result = await analyzeJavaScriptApplication({ input_path: ${JSON.stringify(root)}, format: "directory" });
+ if (!result.ok) throw result.error;
+ const nodes = result.value.normalized_result.semantic_graph.nodes;
+ console.log(JSON.stringify(nodes.filter(node => node.kind === "return-site").map(node => node.function_node_id)));
+ `;
+ const { stdout, stderr } = await promisify(execFile)(
+ process.execPath,
+ ["--input-type=module", "-e", script],
+ {
+ timeout: 10_000,
+ // Bound the child heap while retaining Node's default stack size.
+ env: {
+ HOME: root,
+ TMPDIR: root,
+ NODE_OPTIONS: "--max-old-space-size=768",
+ },
+ },
+ );
+ expect(stderr).toBe("");
+ const owners: unknown = JSON.parse(stdout);
+ expect(owners).toHaveLength(3);
+ expect(new Set(Array.isArray(owners) ? owners : []).size).toBe(3);
+});
From 6375ccaf04b4835cc67080ee1ecd91d1fc46645c Mon Sep 17 00:00:00 2001
From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Date: Fri, 9 Oct 2026 21:35:50 -0700
Subject: [PATCH 19/74] perf(native): record macOS command provenance once in
its Evidence (#1430)
inspect_macho, inspect_signature, inspect_plist, list_architectures and
demangle_swift already return their command provenance inside the
normalized result. The provider also copied the same array into
raw_result, so every response carried it twice. raw_result now stays
null, as docs/mcp-contracts.md describes for results without a distinct
provider representation.
Co-authored-by: Claude Opus 5.5
---
src/native/NativeMacOSProvider.ts | 6 ---
src/native/NativeMachoInspection.ts | 2 -
.../providers/native/nativeProvider.test.ts | 37 +++++++++++++++++++
3 files changed, 37 insertions(+), 8 deletions(-)
diff --git a/src/native/NativeMacOSProvider.ts b/src/native/NativeMacOSProvider.ts
index 12c94814fc..d63db25061 100644
--- a/src/native/NativeMacOSProvider.ts
+++ b/src/native/NativeMacOSProvider.ts
@@ -233,7 +233,6 @@ class NativeMacOSClient implements AnalysisClient {
return observation.ok
? ok(
createAnalysisExecution(observation.value.result, IDENTITY, {
- rawResult: { provenance: observation.value.provenance },
limitations: observation.value.limitations,
locations: observation.value.locations,
}),
@@ -317,7 +316,6 @@ class NativeMacOSClient implements AnalysisClient {
});
return ok({
result: jsonValueSchema.parse(result),
- provenance,
limitations: [],
locations: architectureLocations(result.architectures.items),
});
@@ -351,7 +349,6 @@ class NativeMacOSClient implements AnalysisClient {
});
return ok({
result: jsonValueSchema.parse(result),
- provenance,
limitations: [],
locations: [],
});
@@ -448,7 +445,6 @@ class NativeMacOSClient implements AnalysisClient {
});
return ok({
result: jsonValueSchema.parse(result),
- provenance,
limitations: result.limitations,
locations: [],
});
@@ -611,7 +607,6 @@ class NativeMacOSClient implements AnalysisClient {
});
return ok({
result: jsonValueSchema.parse(result),
- provenance,
limitations: parsed.value.limitations,
locations: [{ kind: "artifact-path", path: plist.value }],
});
@@ -659,7 +654,6 @@ class NativeMacOSClient implements AnalysisClient {
interface NativeObservation {
readonly result: JsonValue;
- readonly provenance: readonly NativeCommandInvocation[];
readonly limitations: readonly string[];
readonly locations: readonly EvidenceLocation[];
}
diff --git a/src/native/NativeMachoInspection.ts b/src/native/NativeMachoInspection.ts
index 417b43fef5..2ef2c95ae9 100644
--- a/src/native/NativeMachoInspection.ts
+++ b/src/native/NativeMachoInspection.ts
@@ -15,7 +15,6 @@ import { parseOtoolLoadCommands } from "./parsers/otool.js";
interface NativeMachoObservation {
readonly result: JsonValue;
- readonly provenance: readonly NativeCommandInvocation[];
readonly limitations: readonly string[];
readonly locations: readonly EvidenceLocation[];
}
@@ -116,7 +115,6 @@ export const inspectNativeMacho = async (
: normalized;
return ok({
result: jsonValueSchema.parse(result),
- provenance: result.provenance,
limitations: result.limitations,
locations: fileOffsetLocations(result, segmentOffset),
});
diff --git a/tests/boundary/providers/native/nativeProvider.test.ts b/tests/boundary/providers/native/nativeProvider.test.ts
index d5be920ba1..70a3b06ad6 100644
--- a/tests/boundary/providers/native/nativeProvider.test.ts
+++ b/tests/boundary/providers/native/nativeProvider.test.ts
@@ -552,6 +552,43 @@ describe("native macOS provider inspection", () => {
});
});
+describe("native macOS provider raw results", () => {
+ it("records command provenance once, in the normalized result", async () => {
+ directory = await createTestTempDirectory("rea-native-");
+ const app = join(directory, "Fixture.app");
+ const executable = join(app, "Contents/MacOS/Fixture");
+ await mkdir(join(app, "Contents/MacOS"), { recursive: true });
+ await writeFile(executable, "fixture");
+ await writeFile(join(app, "Contents/Info.plist"), "fixture");
+ const client = new NativeMacOSProvider(
+ {},
+ new FixtureRunner(),
+ "darwin",
+ ).createClient(await nativeMachoTargetForFile(executable, app));
+ const requests: readonly (readonly [
+ Parameters[0],
+ Parameters[1],
+ ])[] = [
+ ["inspect_macho", {}],
+ ["list_architectures", {}],
+ ["inspect_signature", {}],
+ ["inspect_plist", {}],
+ ["demangle_swift", { symbols: ["$s4Test3fooyyF", "plain_symbol"] }],
+ ];
+
+ for (const [operation, parameters] of requests) {
+ const execution = await client.execute(operation, parameters);
+ if (!execution.ok) throw execution.error;
+ expect(execution.value.rawResult, operation).toBeNull();
+ expect(execution.value.result, operation).toMatchObject({
+ provenance: expect.arrayContaining([
+ expect.objectContaining({ tool: expect.any(String) }),
+ ]),
+ });
+ }
+ });
+});
+
describe("native plist defaults for iOS-style bundles", () => {
it("defaults to the Info.plist the bundle program was resolved from", async () => {
directory = await createTestTempDirectory("rea-native-flat-");
From 0b9815b6e7080081d8fd426ad9710aee36761afa Mon Sep 17 00:00:00 2001
From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Date: Fri, 9 Oct 2026 21:38:52 -0700
Subject: [PATCH 20/74] docs(skill): save CLI application Evidence and read a
summary view (#1429)
The skill's CLI fallback printed the complete application Evidence to
stdout. A real ASAR produces hundreds of megabytes (334 MB for Obsidian),
which an agent then reads into its context. Save the Evidence to a file
and project a summary or module page with inspect-analysis-view instead.
Co-authored-by: Claude Opus 5.5
---
docs/cli.md | 17 +++++++++++++++++
skill-src/reverse-engineer-anything/SKILL.md | 19 +++++++++++++------
src/generatedPackageMetadata.ts | 2 +-
3 files changed, 31 insertions(+), 7 deletions(-)
diff --git a/docs/cli.md b/docs/cli.md
index 2a9863c1e9..245251a286 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -20,6 +20,23 @@ files when neither `--provider` nor `--snapshot` is supplied. See
[JavaScript artifact reconstruction](javascript-artifact-reconstruction.md)
for results, integrity checks and coverage.
+The complete application Evidence can be hundreds of megabytes for a real
+application. When an agent or a person reads the result, save it to a file and
+project a summary, a module page or one module from the saved Evidence:
+
+```bash
+rea analyze-javascript-application /absolute/path/to/app --json > app-evidence.json
+jq -c '{source: {kind: "inline", evidence: .}, view: {kind: "summary"}}' app-evidence.json > app-view.json
+rea inspect-analysis-view app-view.json
+```
+
+For example, a 334 MB Obsidian application Evidence file yields a summary view
+of about 10 KB. Replace the view with
+`{"kind": "page", "collection": "modules", "offset": 0, "limit": 32}` for a
+module page. See
+[JavaScript application workflows](javascript-application-workflows.md) for
+views, feature traces and comparisons that take the same saved Evidence.
+
## Native analysis
Configure [Hopper or Ghidra](installation.md#hopper), or the
diff --git a/skill-src/reverse-engineer-anything/SKILL.md b/skill-src/reverse-engineer-anything/SKILL.md
index d1650f26e8..3eaa239b8d 100644
--- a/skill-src/reverse-engineer-anything/SKILL.md
+++ b/skill-src/reverse-engineer-anything/SKILL.md
@@ -2,7 +2,7 @@
name: reverse-engineer-anything
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
metadata:
- version: "34"
+ version: "35"
---
# REA
@@ -62,13 +62,20 @@ with a version-pinned `rea-agents` package or continue through the CLI.
A concrete CLI fallback for an operator-supplied JavaScript tree or ASAR is:
```bash
-npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
+npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json > app-evidence.json
+jq -c '{source: {kind: "inline", evidence: .}, view: {kind: "summary"}}' app-evidence.json > app-view.json
+npx -y rea-agents@latest inspect-analysis-view app-view.json
```
-No MCP registration or native engine is required. Read the returned Evidence,
-graph, limitations, and unknowns with the same care as an MCP result; the CLI
-returns the Evidence record directly. This fallback does not establish that
-MCP is configured. Native CLI tasks still require their selected engine.
+No MCP registration or native engine is required. The CLI returns the complete
+Evidence record directly, and a real application can produce hundreds of
+megabytes, so save it to a file rather than reading it into context. Start from
+the summary view, then request a module page
+(`{"kind": "page", "collection": "modules", "offset": 0, "limit": 32}`) or one
+module the same way, or pass the saved Evidence inline to
+`trace-application-feature`. Read graph, limitations, and unknowns with the
+same care as an MCP result. This fallback does not establish that MCP is
+configured. Native CLI tasks still require their selected engine.
## Route the target first
diff --git a/src/generatedPackageMetadata.ts b/src/generatedPackageMetadata.ts
index 29177bf02f..468117b055 100644
--- a/src/generatedPackageMetadata.ts
+++ b/src/generatedPackageMetadata.ts
@@ -5,5 +5,5 @@ export const PACKAGE_METADATA = {
serverSdkVersion: "2.3.1",
clientSdkVersion: "2.3.1",
coreSdkVersion: "2.3.1",
- skillVersion: "34",
+ skillVersion: "35",
} as const;
From dc5af6d68f74155d32a0be957c88afb0ee4be699 Mon Sep 17 00:00:00 2001
From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com>
Date: Sat, 10 Oct 2026 12:49:09 +0800
Subject: [PATCH 21/74] perf(mcp): reuse output validators across catalog
refreshes (#1433)
Advertise content- and dialect-bound identities for self-contained output schemas so the SDK can reuse unchanged Ajv validators after complete tools/list refreshes. Preserve explicit IDs and relative reference bases.
Verify changed schemas, availability transitions, reconnects, numeric and selector constraints, explicit IDs, relative references, and advertised dialects through the pinned SDK boundaries.
Refs #1065, #723
---
docs/mcp-contracts.md | 7 ++
src/contracts/toolOutputReferences.test.ts | 58 ++++++++++
src/contracts/toolSchemaMetadata.ts | 44 ++++++-
.../boundary/mcp/outputValidatorReuse.test.ts | 108 ++++++++++++++++++
4 files changed, 213 insertions(+), 4 deletions(-)
create mode 100644 tests/boundary/mcp/outputValidatorReuse.test.ts
diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md
index 7bf2fb76c0..70d843335b 100644
--- a/docs/mcp-contracts.md
+++ b/docs/mcp-contracts.md
@@ -36,6 +36,13 @@ after SDK conversion and check advertised validation and actual calls.
Individual model APIs can impose additional nesting limits; complete producer
captures can exceed ten structural levels.
+Self-contained output schemas advertise a content-bound `$id`, including their
+declared dialect. SDK validators can reuse compiled schemas across complete
+catalog refreshes and equivalent tool outputs; changing the schema changes its
+identity. Explicit schema IDs and relative external reference bases are
+preserved. This keeps every tool and validation rule in discovery and does not
+change the SDK's catalog invalidation or availability checks.
+
`compare_web_captures` accepts exactly one of two input shapes:
- Passive: `before` and `after` each contain `inspection`, the complete
diff --git a/src/contracts/toolOutputReferences.test.ts b/src/contracts/toolOutputReferences.test.ts
index 70c1f5ea5e..cda49f1d5e 100644
--- a/src/contracts/toolOutputReferences.test.ts
+++ b/src/contracts/toolOutputReferences.test.ts
@@ -1,11 +1,69 @@
import { Ajv } from "ajv";
import { Ajv2020 } from "ajv/dist/2020.js";
+import { AjvJsonSchemaValidator } from "@modelcontextprotocol/client/validators/ajv";
import { expect, it } from "vitest";
import { z } from "zod";
import { toolContract } from "./toolContracts.js";
import { toolOutputSchemaWithMetadata } from "./toolSchemaMetadata.js";
+it("keeps dialect-specific validators distinct after JSON schema serialization", () => {
+ const contract = {
+ ...toolContract("binary_session"),
+ outputSchema: z.strictObject({ value: z.number().int().min(1).max(8) }),
+ };
+ const output = toolOutputSchemaWithMetadata(contract);
+ const provider = new AjvJsonSchemaValidator();
+ for (const target of ["draft-2020-12", "draft-07"] as const) {
+ const projected = output["~standard"].jsonSchema.output({ target });
+ const parsed = z
+ .record(z.string(), z.unknown())
+ .parse(JSON.parse(JSON.stringify(projected)));
+ const validate = provider.getValidator(parsed);
+ expect(validate({ value: 8 }).valid).toBe(true);
+ expect(validate({ value: 9 }).valid).toBe(false);
+ expect(validate({ value: 1.5 }).valid).toBe(false);
+ }
+});
+
+it("preserves explicit schema identity and relative reference resolution", () => {
+ const value = z.string();
+ const canonical = z.strictObject({ value });
+ const contract = {
+ ...toolContract("binary_session"),
+ outputSchema: canonical,
+ };
+ const projected = toolOutputSchemaWithMetadata(contract)[
+ "~standard"
+ ].jsonSchema.output({
+ target: "draft-2020-12",
+ libraryOptions: {
+ override: (context: {
+ readonly zodSchema: unknown;
+ readonly jsonSchema: Record;
+ }) => {
+ if (context.zodSchema !== value) return;
+ delete context.jsonSchema.type;
+ context.jsonSchema.$ref = "child.json";
+ },
+ },
+ });
+ const ajv = new Ajv2020({ strict: false });
+ ajv.addSchema({ $id: "child.json", type: "string", minLength: 3 });
+ const validate = ajv.compile(projected);
+ expect(validate({ value: "abc" })).toBe(true);
+ expect(validate({ value: "ab" })).toBe(false);
+
+ const identified = toolOutputSchemaWithMetadata({
+ ...contract,
+ outputSchema: canonical.meta({ $id: "https://example.test/result" }),
+ })["~standard"].jsonSchema.output({ target: "draft-2020-12" });
+ expect(identified.$id).toBe("https://example.test/result");
+ const explicit = ajv.compile(identified);
+ expect(explicit({ value: "abc" })).toBe(true);
+ expect(explicit({ value: 1 })).toBe(false);
+});
+
it.each(["draft-2020-12", "draft-07"] as const)(
"preserves refined numeric bounds and strict repeated objects through %s references",
(target) => {
diff --git a/src/contracts/toolSchemaMetadata.ts b/src/contracts/toolSchemaMetadata.ts
index f2d395ece8..8f43f1708c 100644
--- a/src/contracts/toolSchemaMetadata.ts
+++ b/src/contracts/toolSchemaMetadata.ts
@@ -2,6 +2,7 @@ import { z } from "zod";
import type { ToolContract } from "./toolContractTypes.js";
import { presentInputJsonSchema } from "./inputSchemaPresentation.js";
+import { digestCanonicalValue } from "../domain/canonicalDigest.js";
const PROPERTY_DESCRIPTIONS: Readonly> = {
addresses: "Ordered provider-normalized procedure addresses to analyze.",
@@ -69,20 +70,55 @@ export const toolInputSchemaWithMetadata = (
},
);
-/** Share repeated output definitions without changing canonical validation. */
+/** Share output definitions and content-bound validator identities on the wire. */
export const toolOutputSchemaWithMetadata = (
contract: Contract,
): Contract["outputSchema"] =>
withAdvertisedJsonSchema(
contract.outputSchema,
"output",
- (project) => (options) =>
- project({
+ (project) => (options) => {
+ const shared = project({
...options,
libraryOptions: { reused: "ref", ...options.libraryOptions },
- }),
+ });
+ // An explicit ID owns the reference base. Relative external references
+ // also depend on that base, so leave those projections unchanged.
+ if ("$id" in shared || hasRelativeSchemaReference(shared)) return shared;
+ return {
+ ...shared,
+ $id: `urn:rea:tool-output:sha256:${digestCanonicalValue(
+ { target: options.target, schema: shared },
+ "Tool output schema",
+ )}`,
+ };
+ },
);
+const hasRelativeSchemaReference = (
+ schema: Record,
+): boolean => {
+ const pending: unknown[] = [schema];
+ const visited = new WeakSet
+
diff --git a/website/public/showcase/aegis/index.html b/website/public/showcase/aegis/index.html
index 05a230501a..7949addfd3 100644
--- a/website/public/showcase/aegis/index.html
+++ b/website/public/showcase/aegis/index.html
@@ -108,6 +108,25 @@
/>
+
+
+
@@ -718,7 +737,8 @@ npx rea-agents@latest inspect-android-package \
SourceMIT licenseCommunity
+ >Join the communityFollow us on X
diff --git a/website/public/showcase/ctf/index.html b/website/public/showcase/ctf/index.html
index ed20029247..afe735fee2 100644
--- a/website/public/showcase/ctf/index.html
+++ b/website/public/showcase/ctf/index.html
@@ -107,6 +107,25 @@
/>
+
+
+
@@ -688,7 +707,8 @@ chmod +x ms_flag_checker
SourceMIT licenseCommunity
+ >Join the communityFollow us on X
diff --git a/website/public/showcase/dx-ball/index.html b/website/public/showcase/dx-ball/index.html
index d47449978c..d89d2bf5bc 100644
--- a/website/public/showcase/dx-ball/index.html
+++ b/website/public/showcase/dx-ball/index.html
@@ -107,6 +107,25 @@
/>
+
+
+
@@ -696,7 +715,8 @@
SourceMIT licenseCommunity
+ >Join the communityFollow us on X
diff --git a/website/public/showcase/index.html b/website/public/showcase/index.html
index f0a537f696..168afcde91 100644
--- a/website/public/showcase/index.html
+++ b/website/public/showcase/index.html
@@ -107,6 +107,25 @@
/>
+
+
+
@@ -228,7 +247,8 @@
SourceMIT licenseCommunity
+ >Join the communityFollow us on X
diff --git a/website/public/showcase/notion/index.html b/website/public/showcase/notion/index.html
index 5120d8ca61..c276d16c9b 100644
--- a/website/public/showcase/notion/index.html
+++ b/website/public/showcase/notion/index.html
@@ -107,6 +107,25 @@
/>
+
+
+
@@ -534,7 +553,8 @@ mimeTypes:
SourceMIT licenseCommunity
+ >Join the communityFollow us on X
diff --git a/website/public/showcase/th04/index.html b/website/public/showcase/th04/index.html
index e08f8f2a3c..9a89af6399 100644
--- a/website/public/showcase/th04/index.html
+++ b/website/public/showcase/th04/index.html
@@ -107,6 +107,25 @@
/>
+
+
+
@@ -711,7 +730,8 @@ no_aim:
SourceMIT licenseCommunity
+ >Join the communityFollow us on X
From 934ffd28d15f4fbe6687ca8986a949e1b41a07d1 Mon Sep 17 00:00:00 2001
From: keemsisi
Date: Sat, 10 Oct 2026 08:09:18 +0100
Subject: [PATCH 42/74] perf(javascript): derive the application graph ID once
createJavaScriptApplicationGraph hashed the whole graph to derive its
graph_id, then parsed it with the public schema, whose invariant check
hashed the same content again to verify that ID. Validate the content
with every other invariant first, then derive graph_id once from the
validated copy. The public schema still verifies graph_id for stored
and untrusted graphs, and created graphs are unchanged.
---
.../javascriptApplicationGraph.test.ts | 15 +++++++++++++
.../javascript/javascriptApplicationGraph.ts | 22 ++++++++++++++-----
2 files changed, 31 insertions(+), 6 deletions(-)
diff --git a/src/domain/javascript/javascriptApplicationGraph.test.ts b/src/domain/javascript/javascriptApplicationGraph.test.ts
index efedf78c06..58f1279284 100644
--- a/src/domain/javascript/javascriptApplicationGraph.test.ts
+++ b/src/domain/javascript/javascriptApplicationGraph.test.ts
@@ -547,6 +547,21 @@ describe("JavaScript Application Graph: source maps and observations", () => {
/Edge identifier/u,
);
});
+
+ it("creates exactly the graph the public schema accepts", () => {
+ const graph = buildSyntheticJavaScriptApplicationGraph();
+ expect(JSON.stringify(graph)).toBe(
+ JSON.stringify(
+ javascriptApplicationGraphSchema.parse(structuredClone(graph)),
+ ),
+ );
+
+ const { graph_id: _graphId, ...semantic } = structuredClone(graph);
+ firstOf(semantic.edges, "edge").edge_id = `jag_edge_${"f".repeat(64)}`;
+ expect(() => createJavaScriptApplicationGraph(semantic)).toThrow(
+ /Edge identifier/u,
+ );
+ });
});
describe("JavaScript Application Graph: duplicate and orphan rejection", () => {
diff --git a/src/domain/javascript/javascriptApplicationGraph.ts b/src/domain/javascript/javascriptApplicationGraph.ts
index ba122e1f06..e16aded759 100644
--- a/src/domain/javascript/javascriptApplicationGraph.ts
+++ b/src/domain/javascript/javascriptApplicationGraph.ts
@@ -304,8 +304,8 @@ const checkNode = (
type GraphRecord = z.infer;
-const checkGraphInvariants = (
- graph: GraphRecord,
+const checkGraphContent = (
+ graph: JavaScriptApplicationGraphInput,
context: z.RefinementCtx,
): void => {
sortedUniqueIssue(
@@ -394,7 +394,13 @@ const checkGraphInvariants = (
context,
);
}
+};
+const checkGraphInvariants = (
+ graph: GraphRecord,
+ context: z.RefinementCtx,
+): void => {
+ checkGraphContent(graph, context);
const { graph_id: identifier, ...semantic } = graph;
if (
identifier !==
@@ -411,6 +417,9 @@ const checkGraphInvariants = (
export const javascriptApplicationGraphSchema =
javascriptApplicationGraphRecordSchema.superRefine(checkGraphInvariants);
+const javascriptApplicationGraphContentSchema =
+ javascriptApplicationGraphInputSchema.superRefine(checkGraphContent);
+
/** Fully validated JavaScript Application Graph. */
export type JavaScriptApplicationGraph = z.infer<
typeof javascriptApplicationGraphSchema
@@ -482,10 +491,11 @@ export const createJavaScriptApplicationGraph = (
},
limitations: uniqueSorted(parsed.limitations),
};
- return javascriptApplicationGraphSchema.parse({
- ...semantic,
- graph_id: `jag_${digestCanonicalValue(semantic, "JavaScript Application Graph")}`,
- });
+ const content = javascriptApplicationGraphContentSchema.parse(semantic);
+ return {
+ ...content,
+ graph_id: `jag_${digestCanonicalValue(content, "JavaScript Application Graph")}`,
+ };
};
const validatedImmutableApplicationGraphs = new WeakSet