diff --git a/docs/adr/0001-provider-selection-and-analysis-profiles.md b/docs/adr/0001-provider-selection-and-analysis-profiles.md
index dfcc7f86a..07eaa659e 100644
--- a/docs/adr/0001-provider-selection-and-analysis-profiles.md
+++ b/docs/adr/0001-provider-selection-and-analysis-profiles.md
@@ -30,7 +30,7 @@ analyzer, or provider-version differences while appearing target-compatible.
The decision addressed the earlier representations of
[`CompositeProvider`](https://github.com/morluto/rea/blob/main/src/application/binary/CompositeProvider.ts),
-[runtime composition](https://github.com/morluto/rea/blob/main/src/application/runtime.ts),
+[runtime composition](https://github.com/morluto/rea/blob/main/src/composition/binary.ts),
[`BinarySession`](https://github.com/morluto/rea/blob/main/src/application/binary/BinarySession.ts),
[`BinaryTarget`](https://github.com/morluto/rea/blob/main/src/domain/binaryTarget.ts),
[analysis snapshot](https://github.com/morluto/rea/blob/main/src/domain/analysisSnapshot.ts), and
diff --git a/docs/architecture.mermaid b/docs/architecture.mermaid
index d3430f38e..f4835fdb2 100644
--- a/docs/architecture.mermaid
+++ b/docs/architecture.mermaid
@@ -5,6 +5,10 @@ graph TD
DISPATCH["scripts/rea.mjs
Executable dispatcher
routes mcp / one-shot"]
end
+ subgraph "Production Composition"
+ COMPOSITION["src/composition/
fresh typed provider and session factories
one-shot binding and independent optional loading"]
+ end
+
subgraph "Adapters"
SERVER["src/server/
MCP request translation
createServer, tool registration, toolResult"]
HOPPER_BRIDGE["bridge/hopper_bridge.py
Runs inside Hopper
adapts operations to Hopper Python API"]
@@ -18,7 +22,7 @@ graph TD
REGISTRY["AnalysisProviderRegistry
sorted deep candidates
deterministic selection, no fallback"]
RECORDS["src/application/investigation/
composed InvestigationRecords + narrow ports
EvidenceLedger and Unknown ownership"]
SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache
binary target/profile snapshots
compatibility facade and post-commit notifications"]
- JS_RECON["src/application/ + src/artifacts/
JavaScript artifact reconstruction
directory/ASAR reads
AST-only bundle projection"]
+ JS_RECON["src/application/javascript/ + src/artifacts/javascript/
JavaScript artifact reconstruction
directory/ASAR reads
AST-only bundle projection"]
end
subgraph "Provider Layers"
@@ -69,6 +73,10 @@ graph TD
MAIN --> SERVER
MAIN --> APP
CLI --> APP
+ MAIN --> COMPOSITION
+ CLI --> COMPOSITION
+ SERVER --> COMPOSITION
+ COMPOSITION --> SESSION
APP --> FIRMWARE
FIRMWARE --> PROCESS
FIRMWARE --> CONTRACTS
@@ -84,9 +92,10 @@ graph TD
APP -->|record callbacks| RECORDS
SERVER -->|narrow record ports| RECORDS
RECORDS --> DOMAIN
- REGISTRY --> HOPPER
- REGISTRY --> GHIDRA
- REGISTRY --> IDA
+ COMPOSITION --> HOPPER
+ COMPOSITION --> GHIDRA
+ COMPOSITION --> IDA
+ COMPOSITION --> AUX
IDA --> IDA_MCP
IDA --> CONTRACTS
IDA --> DOMAIN
diff --git a/docs/capability-migration.md b/docs/capability-migration.md
index 3789591d6..9363211f6 100644
--- a/docs/capability-migration.md
+++ b/docs/capability-migration.md
@@ -6,17 +6,17 @@ there is no new universal provider interface or fixed module manifest.
## Ownership to preserve
-| Responsibility | Current owner | Migration direction |
-| --------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------- |
-| Analyst semantics and Evidence representation | domain | Keep pure and provider-neutral |
-| Named public input/output contracts | contracts | Keep exact CLI/MCP meaning and complete discovery |
-| Producer parsing and external tool protocols | provider adapters | Keep behind the relevant typed port |
-| Shared analyst workflows | application | Share between CLI and MCP |
-| Concrete provider construction | binary runtime, composition, MCP startup | Consolidate one capability at a time in production composition |
-| Investigation Evidence and Unknowns | InvestigationRecords/EvidenceLedger | Consume narrow read/write/atomic record ports |
-| Target/profile snapshots and invalidation | BinarySessionRecords/BinarySession | Keep binary-owned |
-| Subprocess ownership and host primitives | process/windows | Reuse; preserve cancellation and cleanup |
-| Public translation | CLI/MCP adapters | Delegate to shared workflows and named contracts |
+| Responsibility | Current owner | Migration direction |
+| --------------------------------------------- | ----------------------------------- | ----------------------------------------------------- |
+| Analyst semantics and Evidence representation | domain | Keep pure and provider-neutral |
+| Named public input/output contracts | contracts | Keep exact CLI/MCP meaning and complete discovery |
+| Producer parsing and external tool protocols | provider adapters | Keep behind the relevant typed port |
+| Shared analyst workflows | application | Share between CLI and MCP |
+| Concrete provider construction | composition, MCP startup | Keep fresh typed factories at the production boundary |
+| Investigation Evidence and Unknowns | InvestigationRecords/EvidenceLedger | Consume narrow read/write/atomic record ports |
+| Target/profile snapshots and invalidation | BinarySessionRecords/BinarySession | Keep binary-owned |
+| Subprocess ownership and host primitives | process/windows | Reuse; preserve cancellation and cleanup |
+| Public translation | CLI/MCP adapters | Delegate to shared workflows and named contracts |
Retain exact Evidence IDs, provenance, detached reads, atomic Evidence/Unknown
mutations and optimistic revisions. Preserve current close and failed-cleanup
@@ -69,8 +69,8 @@ paths removed during a move. Pass exact source paths for a focused check.
Pure source and its tests keep inward dependencies. Application/server provider
construction guards cover the migrated deep, Android, firmware, JavaScript recovery and observation
-implementations. The existing binary runtime is an exact temporary composition
-exception; shared browser capture/export helpers remain admitted while their
+implementations. Application workflows cannot import composition, including the
+former binary runtime entrypoint; shared browser capture/export helpers remain admitted while their
ownership is reviewed. Test lane restrictions still use Oxlint. Producer-dependent
browser and managed comparison tests live in boundary lanes with their original
fixtures and assertions. This is a development guard, not runtime authorization.
@@ -104,8 +104,8 @@ inside the provider. The existing launcher seams remain available to boundary
fixtures, which now exercise the production factories.
Application workflows and MCP registration must not import these concrete
-providers. Composition may import their implementations. Binary composition is
-still at its existing entrypoint while its direct-analysis callers are investigated.
+providers. Composition may import their implementations. Binary composition also
+uses this outer boundary after its direct-analysis callers were migrated.
This concrete pilot does not introduce a shared provider interface or module
manifest.
@@ -329,10 +329,9 @@ records still have their separate owner in `src/application/investigation/`.
`AnalysisProvider.ts` stays shared because its identity/execution types also serve
nonbinary ports. `BinaryTargetResolver.ts` stays shared because Android, firmware,
-managed and artifact workflows use its target parsing. The existing `runtime.ts`
-remains the exact temporary production wiring entrypoint while direct-analysis
-callers are migrated deliberately; application workflows gain no outward
-composition dependency.
+managed and artifact workflows use its target parsing. Production wiring lives
+in `src/composition/binary.ts`; application workflows gain no outward composition
+dependency.
The three colocated application tests move with their owner. Composition cases
remain in `tests/composition/analysis-sessions/`; filesystem and SDK cases retain
@@ -385,3 +384,23 @@ Portable decoder and injected producer tests establish their format/boundary
claims. Actual Swift/Xcode, assetutil, native NIB, bundle signing and DMG lifecycle
claims require the real Apple lane. Native binary API/value-flow semantics and
shared cross-domain contract aggregates keep their separate owners.
+
+## Binary production composition closure
+
+`src/composition/binary.ts` is the shared production owner for deep candidate
+construction, lazy auxiliary composition and the managed-only session factory.
+It preserves the existing constructor bodies, selection policy and fresh session
+per invocation. Factory registration acquires no engine or target.
+
+`DirectAnalysis` and `DirectAnalysisStatus` receive these two existing factories
+through `DirectAnalysisDependencies`. They retain configuration parsing, selected
+status environment, cancellation, snapshot replay/binding, Evidence projection
+and finally-close. The dependency uses the existing BinarySession type, including
+its replay policy; it adds no universal backend or lifecycle contract.
+
+`src/composition/directAnalysis.ts` binds the production factories for CLI and
+one-shot managed MCP callers. Binding creates no session and stores no retained
+analysis state. MCP startup constructs its connection session through the same
+binary factory. Source and compiled fixtures use these entrypoints. The former
+application runtime path is removed, and the source guard no longer admits its
+outward provider/composition imports.
diff --git a/scripts/lib/module-boundaries.mjs b/scripts/lib/module-boundaries.mjs
index ca1e4d933..a0c71738b 100644
--- a/scripts/lib/module-boundaries.mjs
+++ b/scripts/lib/module-boundaries.mjs
@@ -89,12 +89,14 @@ const failedBoundary = (file, target) => {
)
return "pure-layer";
}
- if (file === "src/application/runtime.ts") return undefined;
if (layer === "application" && targetLayer === "composition")
return "application-composition";
if (
(layer === "application" || layer === "server") &&
(MIGRATED_PROVIDER_ROOTS.has(targetLayer) ||
+ /^src\/(?:artifacts\/ArtifactProvider|dotnet\/ManagedStaticProvider|native\/NativeMacOSProvider)\.[^/]+$/u.test(
+ target,
+ ) ||
/^src\/browser\/[^/]*Provider\.[^/]+$/u.test(target))
)
return "provider-construction";
diff --git a/src/application/DirectAnalysis.ts b/src/application/DirectAnalysis.ts
index 5875047bb..c273128f2 100644
--- a/src/application/DirectAnalysis.ts
+++ b/src/application/DirectAnalysis.ts
@@ -3,7 +3,8 @@ import { parseConfig } from "../config.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { EnhancedTools } from "./EnhancedTools.js";
import { executeFunctionAnalysisEvidence } from "./FunctionAnalysisEvidence.js";
-import { createBinarySession, createManagedBinarySession } from "./runtime.js";
+import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js";
+import type { BinarySession } from "./binary/BinarySession.js";
import { silentLogger, type Logger } from "../logger.js";
import { createEvidence } from "../domain/evidence.js";
import type { Evidence } from "../domain/evidence.js";
@@ -38,11 +39,6 @@ import {
import type { AnalysisProviderSelector } from "../contracts/providerSelection.js";
import { artifactInspectionResultSchema } from "../domain/artifactInspection.js";
-export {
- runCapabilityStatus,
- runProviderStatus,
-} from "./DirectAnalysisStatus.js";
-
type DirectAnalysisTool =
| "annotate_native_function"
| "inspect_native_load_image"
@@ -70,6 +66,7 @@ type DirectAnalysisTool =
* retain a target or provider client for a subsequent command.
*/
export const runDirectAnalysis = async (
+ dependencies: DirectAnalysisDependencies,
path: string,
tool: DirectAnalysisTool,
arguments_: Readonly>,
@@ -82,7 +79,7 @@ export const runDirectAnalysis = async (
} = {},
): Promise =>
withProcessCancellation(options.signal, (signal) =>
- runAnalysis(path, tool, arguments_, {
+ runAnalysis(dependencies, path, tool, arguments_, {
logger: options.logger ?? silentLogger,
snapshotPath: options.snapshotPath,
signal,
@@ -97,6 +94,7 @@ export const runDirectAnalysis = async (
/** Execute one provider-native semantic operation with atomic provenance. */
export const runProviderAnalysis = async (
+ dependencies: DirectAnalysisDependencies,
...[path, tool, arguments_, logger = silentLogger, signal]: readonly [
path: string,
tool: NativeToolName | ArtifactAnalysisOperation | ManagedToolName,
@@ -106,9 +104,9 @@ export const runProviderAnalysis = async (
]
): Promise =>
isManagedToolName(tool)
- ? runManagedProviderAnalysis(path, tool, signal)
+ ? runManagedProviderAnalysis(dependencies, path, tool, signal)
: withProcessCancellation(signal, (operationSignal) =>
- runAnalysis(path, tool, arguments_, {
+ runAnalysis(dependencies, path, tool, arguments_, {
logger,
snapshotPath: undefined,
signal: operationSignal,
@@ -117,12 +115,13 @@ export const runProviderAnalysis = async (
/** Execute managed metadata inspection in an isolated managed-only session. */
export const runManagedProviderExecution = async (
+ dependencies: DirectAnalysisDependencies,
path: string,
tool: ManagedToolName,
signal?: AbortSignal,
): Promise> =>
withProcessCancellation(signal, async (operationSignal) => {
- const session = createManagedBinarySession();
+ const session = dependencies.createManagedBinarySession();
try {
const opened = await session.open(path, { signal: operationSignal });
if (!opened.ok) return opened;
@@ -133,11 +132,17 @@ export const runManagedProviderExecution = async (
});
const runManagedProviderAnalysis = async (
+ dependencies: DirectAnalysisDependencies,
path: string,
tool: ManagedToolName,
signal?: AbortSignal,
): Promise => {
- const execution = await runManagedProviderExecution(path, tool, signal);
+ const execution = await runManagedProviderExecution(
+ dependencies,
+ path,
+ tool,
+ signal,
+ );
if (!execution.ok) return cliError(execution.error);
const value = execution.value;
return createEvidence(value.subject ?? undefined, value.provider, {
@@ -151,6 +156,7 @@ const runManagedProviderAnalysis = async (
};
const runAnalysis = async (
+ dependencies: DirectAnalysisDependencies,
path: string,
tool:
| NativeToolName
@@ -177,7 +183,7 @@ const runAnalysis = async (
const { logger, signal, snapshotPath } = options;
const config = parseConfig(options.environment ?? process.env);
if (!config.ok) return cliError(config.error);
- const session = createBinarySession(config.value, logger);
+ const session = dependencies.createBinarySession(config.value, logger);
try {
const prepared = await prepareSnapshot({
path,
@@ -247,13 +253,13 @@ const runAnalysis = async (
};
const canReplayDirectTool = (
- session: ReturnType,
- tool: Parameters[1],
+ session: BinarySession,
+ tool: Parameters[2],
): boolean =>
tool !== "trace_native_ui_action" && session.allowsSnapshotReplay(tool);
const executeAnalysisTool = async (input: {
- readonly session: ReturnType;
+ readonly session: BinarySession;
readonly openedTarget: BinaryTarget;
readonly tool:
| NativeToolName
@@ -372,7 +378,7 @@ const prepareSnapshot = async (options: {
};
const replayProviderFor = (
- session: ReturnType,
+ session: BinarySession,
tool:
| NativeToolName
| ArtifactAnalysisOperation
@@ -384,7 +390,7 @@ const replayProviderFor = (
: session.providerIdentity(tool);
const analysisProfileForEvidence = (
- session: ReturnType,
+ session: BinarySession,
tool:
| NativeToolName
| ArtifactAnalysisOperation
diff --git a/src/application/DirectAnalysisDependencies.ts b/src/application/DirectAnalysisDependencies.ts
new file mode 100644
index 000000000..606b8eaa4
--- /dev/null
+++ b/src/application/DirectAnalysisDependencies.ts
@@ -0,0 +1,12 @@
+import type { AppConfig } from "../config.js";
+import type { Logger } from "../logger.js";
+import type { BinarySession } from "./binary/BinarySession.js";
+
+/** Existing one-shot session factories supplied by the production boundary. */
+export interface DirectAnalysisDependencies {
+ readonly createBinarySession: (
+ config: AppConfig,
+ logger: Logger,
+ ) => BinarySession;
+ readonly createManagedBinarySession: () => BinarySession;
+}
diff --git a/src/application/DirectAnalysisStatus.ts b/src/application/DirectAnalysisStatus.ts
index 142eccfff..4d75f831f 100644
--- a/src/application/DirectAnalysisStatus.ts
+++ b/src/application/DirectAnalysisStatus.ts
@@ -3,15 +3,16 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
import { createServerIdentity } from "../serverIdentity.js";
import { silentLogger, type Logger } from "../logger.js";
-import { createBinarySession } from "./runtime.js";
+import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js";
const runSessionStatus = async (
+ dependencies: Pick,
logger: Logger = silentLogger,
environment: Readonly> = process.env,
): Promise => {
const config = parseConfig(environment);
if (!config.ok) return { error: projectAnalysisError(config.error) };
- const session = createBinarySession(config.value, logger);
+ const session = dependencies.createBinarySession(config.value, logger);
try {
return {
...jsonObjectSchema.parse(session.status()),
@@ -26,12 +27,14 @@ const runSessionStatus = async (
/** List binary-session provider candidates and auxiliary operation availability. */
export const runProviderStatus = (
+ dependencies: Pick,
logger: Logger = silentLogger,
environment: Readonly> = process.env,
-) => runSessionStatus(logger, environment);
+) => runSessionStatus(dependencies, logger, environment);
/** List binary-session operation descriptors; this is not the full MCP catalog. */
export const runCapabilityStatus = (
+ dependencies: Pick,
logger: Logger = silentLogger,
environment: Readonly> = process.env,
-) => runSessionStatus(logger, environment);
+) => runSessionStatus(dependencies, logger, environment);
diff --git a/src/cli/artifactCommands.ts b/src/cli/artifactCommands.ts
index 6eaecab5f..4681608e0 100644
--- a/src/cli/artifactCommands.ts
+++ b/src/cli/artifactCommands.ts
@@ -1,6 +1,6 @@
import { z } from "incur";
-import { runProviderAnalysis } from "../application/DirectAnalysis.js";
+import { runProviderAnalysis } from "../composition/directAnalysis.js";
import { createArtifactExtractionDestination } from "../application/ArtifactExtractionDestination.js";
import { logCliCommand } from "../cliLogging.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
diff --git a/src/cli/coreBinaryCommands.ts b/src/cli/coreBinaryCommands.ts
index 0a095cc95..6c70a2626 100644
--- a/src/cli/coreBinaryCommands.ts
+++ b/src/cli/coreBinaryCommands.ts
@@ -2,7 +2,7 @@ import { z } from "incur";
import { stat } from "node:fs/promises";
import { resolve } from "node:path";
-import { runDirectAnalysis } from "../application/DirectAnalysis.js";
+import { runDirectAnalysis } from "../composition/directAnalysis.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import type { Logger } from "../logger.js";
diff --git a/src/cli/coreNativeCommands.ts b/src/cli/coreNativeCommands.ts
index 79e7df100..41f8f7655 100644
--- a/src/cli/coreNativeCommands.ts
+++ b/src/cli/coreNativeCommands.ts
@@ -1,6 +1,6 @@
import { z } from "incur";
-import { runDirectAnalysis } from "../application/DirectAnalysis.js";
+import { runDirectAnalysis } from "../composition/directAnalysis.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import type { Logger } from "../logger.js";
diff --git a/src/cli/managedCommands.ts b/src/cli/managedCommands.ts
index b3c167efd..ee32fd91f 100644
--- a/src/cli/managedCommands.ts
+++ b/src/cli/managedCommands.ts
@@ -3,7 +3,7 @@ import { z } from "incur";
import { compareManagedMemberPaths } from "../application/managed/ManagedMemberComparisonService.js";
import { verifyManagedNativeBoundariesEvidence } from "../application/managed/ManagedNativeVerificationService.js";
import { importManagedReconstructionEvidence } from "../application/managed/ManagedReconstructionService.js";
-import { runProviderAnalysis } from "../application/DirectAnalysis.js";
+import { runProviderAnalysis } from "../composition/directAnalysis.js";
import { parseCliJsonInput } from "../cliJsonInput.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
diff --git a/src/cli/utilityCommands.ts b/src/cli/utilityCommands.ts
index c54f20589..6b03ed6a3 100644
--- a/src/cli/utilityCommands.ts
+++ b/src/cli/utilityCommands.ts
@@ -5,7 +5,7 @@ import {
runCapabilityStatus,
runProviderAnalysis,
runProviderStatus,
-} from "../application/DirectAnalysis.js";
+} from "../composition/directAnalysis.js";
import { importReferenceSource } from "../application/ReferenceSourceImport.js";
import { projectReferenceSourceImportError } from "../application/ReferenceSourceImportTypes.js";
import { parseConfig } from "../config.js";
diff --git a/src/cliEnvironmentSeam.test.ts b/src/cliEnvironmentSeam.test.ts
index f53ef55ba..4aa57dca3 100644
--- a/src/cliEnvironmentSeam.test.ts
+++ b/src/cliEnvironmentSeam.test.ts
@@ -7,7 +7,7 @@ import { describe, expect, it } from "vitest";
import { createCli } from "./cli.js";
import { createSystemDoctorHost } from "./doctorRuntime.js";
import { captureProcessScenarioFile } from "./application/ProcessCli.js";
-import { runCapabilityStatus } from "./application/DirectAnalysisStatus.js";
+import { runCapabilityStatus } from "./composition/directAnalysis.js";
import { probeProcessCaptureCapability } from "./application/ProcessHarness.js";
import { parseEvidence } from "./domain/evidence.js";
import { parseProcessCapture } from "./domain/processCapture.js";
diff --git a/src/application/runtime.ts b/src/composition/binary.ts
similarity index 72%
rename from src/application/runtime.ts
rename to src/composition/binary.ts
index 07026076a..5e49cf1ae 100644
--- a/src/application/runtime.ts
+++ b/src/composition/binary.ts
@@ -1,15 +1,15 @@
import type { AppConfig } from "../config.js";
-import type { BinarySession } from "./binary/BinarySession.js";
+import type { BinarySession } from "../application/binary/BinarySession.js";
import { HopperProvider } from "../hopper/HopperProvider.js";
import { GhidraProvider } from "../ghidra/GhidraProvider.js";
import { IdaProvider } from "../ida/IdaProvider.js";
import { silentLogger, type Logger } from "../logger.js";
-import { auxiliaryAnalysisProviderDeclarations } from "../composition/auxiliaryAnalysisProviders.js";
-import { AnalysisProviderRegistry } from "./binary/AnalysisProviderRegistry.js";
-import { composeBinarySession } from "./binary/BinarySessionComposition.js";
-import { LazyAnalysisProvider } from "./binary/LazyAnalysisProvider.js";
+import { auxiliaryAnalysisProviderDeclarations } from "./auxiliaryAnalysisProviders.js";
+import { AnalysisProviderRegistry } from "../application/binary/AnalysisProviderRegistry.js";
+import { composeBinarySession } from "../application/binary/BinarySessionComposition.js";
+import { LazyAnalysisProvider } from "../application/binary/LazyAnalysisProvider.js";
import { ManagedStaticProvider } from "../dotnet/ManagedStaticProvider.js";
-import { SessionProviderRouter } from "./binary/SessionProviderRouter.js";
+import { SessionProviderRouter } from "../application/binary/SessionProviderRouter.js";
/**
* Compose the target-switching runtime shared directly by CLI and MCP adapters.
diff --git a/src/composition/directAnalysis.ts b/src/composition/directAnalysis.ts
new file mode 100644
index 000000000..670d3a642
--- /dev/null
+++ b/src/composition/directAnalysis.ts
@@ -0,0 +1,46 @@
+import {
+ runDirectAnalysis as executeDirectAnalysis,
+ runProviderAnalysis as executeProviderAnalysis,
+ runManagedProviderExecution as executeManagedProvider,
+} from "../application/DirectAnalysis.js";
+import {
+ runCapabilityStatus as executeCapabilityStatus,
+ runProviderStatus as executeProviderStatus,
+} from "../application/DirectAnalysisStatus.js";
+import type { DirectAnalysisDependencies } from "../application/DirectAnalysisDependencies.js";
+import { createBinarySession, createManagedBinarySession } from "./binary.js";
+
+const dependencies: DirectAnalysisDependencies = {
+ createBinarySession,
+ createManagedBinarySession,
+};
+
+/** Run one isolated binary operation through the production session factory. */
+export const runDirectAnalysis = executeDirectAnalysis.bind(
+ undefined,
+ dependencies,
+);
+
+/** Run one isolated native, artifact or managed operation. */
+export const runProviderAnalysis = executeProviderAnalysis.bind(
+ undefined,
+ dependencies,
+);
+
+/** Execute managed metadata without constructing native provider candidates. */
+export const runManagedProviderExecution = executeManagedProvider.bind(
+ undefined,
+ dependencies,
+);
+
+/** Report production provider candidates without opening a target. */
+export const runProviderStatus = executeProviderStatus.bind(
+ undefined,
+ dependencies,
+);
+
+/** Report production session operations without opening a target. */
+export const runCapabilityStatus = executeCapabilityStatus.bind(
+ undefined,
+ dependencies,
+);
diff --git a/src/main.ts b/src/main.ts
index 4f8c6491a..c3b6c74b9 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -6,7 +6,7 @@ import { realpathSync } from "node:fs";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { parseConfig } from "./config.js";
-import { createBinarySession } from "./application/runtime.js";
+import { createBinarySession } from "./composition/binary.js";
import { createLogger } from "./logger.js";
import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
import type { RuntimeDependencies } from "./main/types.js";
diff --git a/src/server/registerManagedTools.ts b/src/server/registerManagedTools.ts
index 66d5d54c8..bf5a9108f 100644
--- a/src/server/registerManagedTools.ts
+++ b/src/server/registerManagedTools.ts
@@ -9,7 +9,7 @@ import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { err } from "../domain/result.js";
import type { Logger } from "../logger.js";
import { registerEvidenceTools } from "./registerEvidenceTools.js";
-import { runManagedProviderExecution } from "../application/DirectAnalysis.js";
+import { runManagedProviderExecution } from "../composition/directAnalysis.js";
import { isManagedToolName } from "../contracts/managed/managedToolContracts.js";
/** Register execution-free managed PE/CLI inspection. */
diff --git a/tests/boundary/filesystem/directAnalysisPermissions.test.ts b/tests/boundary/filesystem/directAnalysisPermissions.test.ts
index 7078d7603..fcdef7e5d 100644
--- a/tests/boundary/filesystem/directAnalysisPermissions.test.ts
+++ b/tests/boundary/filesystem/directAnalysisPermissions.test.ts
@@ -6,7 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { writeAnalysisSnapshot } from "../../../src/application/binary/AnalysisSnapshotFiles.js";
-import { runDirectAnalysis } from "../../../src/application/DirectAnalysis.js";
+import { runDirectAnalysis } from "../../../src/composition/directAnalysis.js";
import type { AnalysisSnapshot } from "../../../src/domain/analysisSnapshot.js";
import {
snapshotBinding,
diff --git a/tests/boundary/filesystem/runtimeIdentification.test.ts b/tests/boundary/filesystem/runtimeIdentification.test.ts
index b9c3c438d..7ebc2e6b9 100644
--- a/tests/boundary/filesystem/runtimeIdentification.test.ts
+++ b/tests/boundary/filesystem/runtimeIdentification.test.ts
@@ -11,7 +11,7 @@ import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
-import { runProviderAnalysis } from "../../../src/application/DirectAnalysis.js";
+import { runProviderAnalysis } from "../../../src/composition/directAnalysis.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import {
identifyRuntimes,
diff --git a/tests/boundary/mcp/artifactEntryOrderMcp.test.ts b/tests/boundary/mcp/artifactEntryOrderMcp.test.ts
index 9fe438994..a3ad28671 100644
--- a/tests/boundary/mcp/artifactEntryOrderMcp.test.ts
+++ b/tests/boundary/mcp/artifactEntryOrderMcp.test.ts
@@ -5,7 +5,7 @@ import { expect, it } from "vitest";
import { z } from "zod";
import { parseConfig } from "../../../src/config.js";
-import { createBinarySession } from "../../../src/application/runtime.js";
+import { createBinarySession } from "../../../src/composition/binary.js";
import { createServer } from "../../../src/server/createServer.js";
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js";
diff --git a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts
index b099a3825..a0bf85875 100644
--- a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts
+++ b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts
@@ -6,7 +6,7 @@ import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
import { expect, it } from "vitest";
import { z } from "zod";
import { parseConfig } from "../../../src/config.js";
-import { createBinarySession } from "../../../src/application/runtime.js";
+import { createBinarySession } from "../../../src/composition/binary.js";
import { keyedArchiveResultSchema } from "../../../src/domain/apple/keyedArchive.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
diff --git a/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts b/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts
index c7e1ab0e7..7c758effb 100644
--- a/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts
+++ b/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts
@@ -10,7 +10,7 @@ import {
} from "@zip.js/zip.js";
import { afterEach, expect, it } from "vitest";
-import { runProviderAnalysis } from "../../../src/application/DirectAnalysis.js";
+import { runProviderAnalysis } from "../../../src/composition/directAnalysis.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
diff --git a/tests/boundary/mcp/nativeDemangleMcp.test.ts b/tests/boundary/mcp/nativeDemangleMcp.test.ts
index 172600ba7..706826ca6 100644
--- a/tests/boundary/mcp/nativeDemangleMcp.test.ts
+++ b/tests/boundary/mcp/nativeDemangleMcp.test.ts
@@ -4,7 +4,7 @@ import { join } from "node:path";
import { describe, expect } from "vitest";
import { z } from "zod";
-import { createBinarySession } from "../../../src/application/runtime.js";
+import { createBinarySession } from "../../../src/composition/binary.js";
import { parseConfig } from "../../../src/config.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
diff --git a/tests/boundary/mcp/shutdown.test.ts b/tests/boundary/mcp/shutdown.test.ts
index 82ce3bdf1..acccf5a32 100644
--- a/tests/boundary/mcp/shutdown.test.ts
+++ b/tests/boundary/mcp/shutdown.test.ts
@@ -2,7 +2,7 @@ import type { StdioServerHandle } from "@modelcontextprotocol/server/stdio";
import pino from "pino";
import { expect, it } from "vitest";
-import { createManagedBinarySession } from "../../../src/application/runtime.js";
+import { createManagedBinarySession } from "../../../src/composition/binary.js";
import { MCP_SHUTDOWN_FAILED } from "../../../src/main/messages.js";
import { createShutdown } from "../../../src/main/shutdown.js";
import type { RuntimeDependencies } from "../../../src/main/types.js";
diff --git a/tests/boundary/providers/managed/managedDirectAnalysis.test.ts b/tests/boundary/providers/managed/managedDirectAnalysis.test.ts
index a1f2b56fa..9abcf917b 100644
--- a/tests/boundary/providers/managed/managedDirectAnalysis.test.ts
+++ b/tests/boundary/providers/managed/managedDirectAnalysis.test.ts
@@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
-import { runProviderAnalysis } from "../../../../src/application/DirectAnalysis.js";
+import { runProviderAnalysis } from "../../../../src/composition/directAnalysis.js";
import { parseEvidence } from "../../../../src/domain/evidence.js";
import { buildManagedPeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js";
diff --git a/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts b/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts
index 33fd99120..f58c5032b 100644
--- a/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts
+++ b/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts
@@ -7,7 +7,7 @@ import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
-import { runProviderAnalysis } from "../../../../src/application/DirectAnalysis.js";
+import { runProviderAnalysis } from "../../../../src/composition/directAnalysis.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { artifactInventoryResultSchema } from "../../../../src/domain/artifactGraph.js";
import { artifactInspectionResultSchema } from "../../../../src/domain/artifactInspection.js";
diff --git a/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts b/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
index 223811110..98ab35808 100644
--- a/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
+++ b/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
@@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
-import { runProviderAnalysis } from "../../../../src/application/DirectAnalysis.js";
+import { runProviderAnalysis } from "../../../../src/composition/directAnalysis.js";
import {
ArtifactPathRegistry,
normalizeArtifactPath,
diff --git a/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts b/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts
index 4966c6ead..c99d2abee 100644
--- a/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts
+++ b/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts
@@ -5,7 +5,7 @@ import { createPackageWithOptions } from "@electron/asar";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
-import { runProviderAnalysis } from "../../../../src/application/DirectAnalysis.js";
+import { runProviderAnalysis } from "../../../../src/composition/directAnalysis.js";
import { artifactInspectionResultSchema } from "../../../../src/domain/artifactInspection.js";
import { parseEvidence } from "../../../../src/domain/evidence.js";
diff --git a/tests/composition/analysis-sessions/appleApplication.test.ts b/tests/composition/analysis-sessions/appleApplication.test.ts
index 8f870ad47..7fca0260c 100644
--- a/tests/composition/analysis-sessions/appleApplication.test.ts
+++ b/tests/composition/analysis-sessions/appleApplication.test.ts
@@ -17,7 +17,7 @@ import {
} from "../../support/applicationSessionFixture.js";
import { projectAppleApplicationEvidence } from "../../../src/application/apple/AppleApplicationService.js";
-import { runProviderAnalysis } from "../../../src/application/DirectAnalysis.js";
+import { runProviderAnalysis } from "../../../src/composition/directAnalysis.js";
import { appleApplicationProjectionResultSchema } from "../../../src/domain/apple/appleApplication.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
diff --git a/tests/composition/android/application.test.ts b/tests/composition/android/application.test.ts
index d0d57be6b..fe8def8ad 100644
--- a/tests/composition/android/application.test.ts
+++ b/tests/composition/android/application.test.ts
@@ -16,7 +16,7 @@ import {
} from "../../support/applicationSessionFixture.js";
import { projectAndroidApplicationEvidence } from "../../../src/application/android/AndroidApplicationService.js";
-import { runProviderAnalysis } from "../../../src/application/DirectAnalysis.js";
+import { runProviderAnalysis } from "../../../src/composition/directAnalysis.js";
import { androidApplicationProjectionResultSchema } from "../../../src/domain/android/androidApplication.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
diff --git a/tests/composition/directAnalysisFactories.test.ts b/tests/composition/directAnalysisFactories.test.ts
new file mode 100644
index 000000000..2de45c68e
--- /dev/null
+++ b/tests/composition/directAnalysisFactories.test.ts
@@ -0,0 +1,152 @@
+import { describe, expect, it } from "vitest";
+import {
+ runDirectAnalysis,
+ runManagedProviderExecution,
+} from "../../src/application/DirectAnalysis.js";
+import { runCapabilityStatus } from "../../src/application/DirectAnalysisStatus.js";
+import type { DirectAnalysisDependencies } from "../../src/application/DirectAnalysisDependencies.js";
+import type { BinarySession } from "../../src/application/binary/BinarySession.js";
+import { parseEvidence } from "../../src/domain/evidence.js";
+import { observed } from "../fixtures/analysisExecution.js";
+import {
+ createBinarySessionTargets,
+ createTestBinarySession,
+} from "../fixtures/binarySession.js";
+
+const recordingFactories = (failure?: Error) => {
+ const sessions: BinarySession[] = [];
+ let clientsClosed = 0;
+ const createSession = () => {
+ const session = createTestBinarySession(() => ({
+ execute: (operation) => {
+ if (failure !== undefined && operation === "read_bytes")
+ return Promise.reject(failure);
+ return Promise.resolve(observed({ operation }));
+ },
+ close: () => {
+ clientsClosed += 1;
+ return Promise.resolve();
+ },
+ }));
+ sessions.push(session);
+ return session;
+ };
+ const dependencies: DirectAnalysisDependencies = {
+ createBinarySession: createSession,
+ createManagedBinarySession: createSession,
+ };
+ return { dependencies, sessions, clientsClosed: () => clientsClosed };
+};
+
+describe("one-shot analysis factory boundary", () => {
+ it("keeps independent commands and releases their actual session clients", async () => {
+ const [path] = await createBinarySessionTargets();
+ const factories = recordingFactories();
+ const first = parseEvidence(
+ await runDirectAnalysis(factories.dependencies, path, "read_bytes", {}),
+ );
+ const second = parseEvidence(
+ await runDirectAnalysis(factories.dependencies, path, "read_bytes", {}),
+ );
+ expect(first).toMatchObject({
+ operation: "read_bytes",
+ normalized_result: { operation: "read_bytes" },
+ subject: { local_path: path },
+ });
+ expect(second.subject).toEqual(first.subject);
+ expect(factories.sessions).toHaveLength(2);
+ expect(factories.sessions[0]).not.toBe(factories.sessions[1]);
+ expect(factories.clientsClosed()).toBe(2);
+ for (const session of factories.sessions) {
+ expect(session.activeTarget()).toBeUndefined();
+ expect(session.evidenceById(first.evidence_id)).toBeUndefined();
+ expect(session.evidenceById(second.evidence_id)).toBeUndefined();
+ }
+ });
+
+ it("preserves thrown failures while releasing clients and cancellation listeners", async () => {
+ const [path] = await createBinarySessionTargets();
+ const failure = new Error("fixture execution failed");
+ const factories = recordingFactories(failure);
+ const listeners = process.listenerCount("SIGINT");
+ await expect(
+ runDirectAnalysis(factories.dependencies, path, "read_bytes", {}),
+ ).rejects.toBe(failure);
+ expect(factories.clientsClosed()).toBe(1);
+ expect(process.listenerCount("SIGINT")).toBe(listeners);
+ expect(factories.sessions[0]?.activeTarget()).toBeUndefined();
+ });
+
+ it("does not acquire a client after cancellation before target opening", async () => {
+ const [path] = await createBinarySessionTargets();
+ const factories = recordingFactories();
+ const controller = new AbortController();
+ controller.abort();
+ const result = await runDirectAnalysis(
+ factories.dependencies,
+ path,
+ "read_bytes",
+ {},
+ { signal: controller.signal },
+ );
+ expect(result).toMatchObject({
+ error: "Analysis failed",
+ code: "cancelled",
+ });
+ expect(factories.clientsClosed()).toBe(0);
+ expect(factories.sessions[0]?.activeTarget()).toBeUndefined();
+ });
+
+ it("keeps managed execution independent of native construction", async () => {
+ const [path] = await createBinarySessionTargets();
+ const factories = recordingFactories();
+ const result = await runManagedProviderExecution(
+ {
+ ...factories.dependencies,
+ createBinarySession: () => {
+ throw new Error("native factory must stay unused");
+ },
+ },
+ path,
+ "inspect_managed_artifact",
+ );
+ expect(result).toMatchObject({
+ ok: true,
+ value: { result: { operation: "inspect_managed_artifact" } },
+ });
+ expect(factories.clientsClosed()).toBe(1);
+ expect(factories.sessions[0]?.activeTarget()).toBeUndefined();
+ });
+});
+
+describe("status factory configuration", () => {
+ it("passes the selected environment and logger without opening a target", async () => {
+ const factories = recordingFactories();
+ const selected: string[] = [];
+ await runCapabilityStatus(
+ {
+ createBinarySession: (config, logger) => {
+ selected.push(config.analysisProvider);
+ expect(logger).toBeDefined();
+ return factories.dependencies.createBinarySession(config, logger);
+ },
+ },
+ undefined,
+ { REA_ANALYSIS_PROVIDER: "ghidra" },
+ );
+ expect(selected).toEqual(["ghidra"]);
+ expect(factories.clientsClosed()).toBe(0);
+ expect(factories.sessions[0]?.activeTarget()).toBeUndefined();
+ });
+
+ it("retains configuration diagnostics before acquiring any session", async () => {
+ const factories = recordingFactories();
+ const result = await runCapabilityStatus(
+ factories.dependencies,
+ undefined,
+ { REA_ANALYSIS_PROVIDER: "invalid_provider" },
+ );
+ expect(result).toHaveProperty("error");
+ expect(factories.sessions).toEqual([]);
+ });
+});
diff --git a/tests/evaluation/moduleBoundaries.test.ts b/tests/evaluation/moduleBoundaries.test.ts
index 4b808f331..8dca27063 100644
--- a/tests/evaluation/moduleBoundaries.test.ts
+++ b/tests/evaluation/moduleBoundaries.test.ts
@@ -8,6 +8,40 @@ import { describe, expect, it } from "vitest";
import { inspectModuleBoundaries } from "../../scripts/lib/module-boundaries.mjs";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
+describe("binary production construction ownership", () => {
+ it.each([
+ "hopper/HopperProvider.js",
+ "ghidra/GhidraProvider.js",
+ "ida/IdaProvider.js",
+ "artifacts/ArtifactProvider.js",
+ "dotnet/ManagedStaticProvider.js",
+ "native/NativeMacOSProvider.js",
+ ])("keeps %s in production composition", (provider) => {
+ const source = `import { Provider } from "../${provider}";`;
+ for (const file of ["src/application/runtime.ts", "src/server/probe.ts"])
+ expect(
+ inspectModuleBoundaries(file, source, process.cwd()),
+ ).toMatchObject([{ boundary: "provider-construction" }]);
+ expect(
+ inspectModuleBoundaries(
+ "src/composition/binary.ts",
+ source,
+ process.cwd(),
+ ),
+ ).toEqual([]);
+ });
+
+ it("rejects the former runtime composition exception", () => {
+ expect(
+ inspectModuleBoundaries(
+ "src/application/runtime.ts",
+ 'export { createBinarySession } from "../composition/binary.js";',
+ process.cwd(),
+ ),
+ ).toMatchObject([{ boundary: "application-composition" }]);
+ });
+});
+
const execute = promisify(execFile);
const verifier = resolve("scripts/verify-module-boundaries.mjs");
@@ -93,7 +127,7 @@ describe("incremental module import boundaries", () => {
"../../android/JadxProvider.js",
false,
],
- ["src/application/runtime.ts", "../android/JadxProvider.js", true],
+ ["src/application/runtime.ts", "../android/JadxProvider.js", false],
[
"src/application/android/runtime.ts",
"../../android/JadxProvider.js",
diff --git a/tests/fixtures/contractPresentation.mjs b/tests/fixtures/contractPresentation.mjs
index 6eab97d88..530f2de23 100644
--- a/tests/fixtures/contractPresentation.mjs
+++ b/tests/fixtures/contractPresentation.mjs
@@ -6,7 +6,7 @@ import { ELECTRON_TOOL_CONTRACTS } from "../../dist/contracts/javascript/electro
import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "../../dist/contracts/javascript/javascriptRuntimeObservationToolContracts.js";
import { TOOL_CONTRACTS } from "../../dist/contracts/toolContracts.js";
import { parseConfig } from "../../dist/config.js";
-import { createBinarySession } from "../../dist/application/runtime.js";
+import { createBinarySession } from "../../dist/composition/binary.js";
// Mutate only this child's presentation arrays, before importing registration.
if (process.argv[2] === "reversed") {
diff --git a/tests/support/applicationSessionFixture.ts b/tests/support/applicationSessionFixture.ts
index d7a23371a..20440ae55 100644
--- a/tests/support/applicationSessionFixture.ts
+++ b/tests/support/applicationSessionFixture.ts
@@ -4,7 +4,7 @@ import { join } from "node:path";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
import { expect } from "vitest";
-import { runProviderAnalysis } from "../../src/application/DirectAnalysis.js";
+import { runProviderAnalysis } from "../../src/composition/directAnalysis.js";
import type { AnalysisError } from "../../src/domain/analysisErrorBase.js";
import { type Evidence, parseEvidence } from "../../src/domain/evidence.js";
import type { Result } from "../../src/domain/result.js";