feat: expand cross-platform investigation workflows

This commit is contained in:
morluto committed 2026-07-20 08:08:39 +00:00
1 parent b537ebfa34
commit dffc81c865
672 files changed
+13013 -2302

No files matched your search

+40
View File
@@ -124,6 +124,11 @@ const SOURCE_PATHS = {
artifactContracts: "dist/contracts/artifactToolContracts.js",
managedContracts: "dist/contracts/managedToolContracts.js",
managedWorkflowContracts: "dist/contracts/managedWorkflowToolContracts.js",
appleApplicationContracts: "dist/contracts/appleApplicationToolContracts.js",
androidApplicationContracts:
"dist/contracts/androidApplicationToolContracts.js",
runtimeIdentificationContracts:
"dist/contracts/runtimeIdentificationToolContracts.js",
browserContracts: "dist/contracts/browserToolContracts.js",
electronContracts: "dist/contracts/electronToolContracts.js",
applicationContracts: "dist/contracts/applicationToolContracts.js",
@@ -200,6 +205,25 @@ const toolFamilyCatalog = (sources) => {
...sources.managedWorkflowContracts.MANAGED_WORKFLOW_TOOL_CONTRACTS,
],
},
{
id: "apple",
surface: "apple-application-workflow",
contracts:
sources.appleApplicationContracts.APPLE_APPLICATION_TOOL_CONTRACTS,
},
{
id: "android",
surface: "android-application-workflow",
contracts:
sources.androidApplicationContracts.ANDROID_APPLICATION_TOOL_CONTRACTS,
},
{
id: "runtime",
surface: "runtime-identification",
contracts:
sources.runtimeIdentificationContracts
.RUNTIME_IDENTIFICATION_TOOL_CONTRACTS,
},
{
id: "browser",
surface: "browser-provider",
@@ -285,6 +309,22 @@ const providerCatalog = (sources) => {
contracts:
sources.managedWorkflowContracts.MANAGED_WORKFLOW_TOOL_CONTRACTS,
},
{
identity: sources.artifactProviders.APPLE_APPLICATION_PROVIDER,
contracts:
sources.appleApplicationContracts.APPLE_APPLICATION_TOOL_CONTRACTS,
},
{
identity: sources.artifactProviders.ANDROID_APPLICATION_PROVIDER,
contracts:
sources.androidApplicationContracts.ANDROID_APPLICATION_TOOL_CONTRACTS,
},
{
identity: sources.artifactProviders.RUNTIME_IDENTIFICATION_PROVIDER,
contracts:
sources.runtimeIdentificationContracts
.RUNTIME_IDENTIFICATION_TOOL_CONTRACTS,
},
{
identity: sources.browserProvider.CDP_BROWSER_PROVIDER_IDENTITY,
contracts: sources.browserContracts.BROWSER_TOOL_CONTRACTS,
+25
View File
@@ -20,13 +20,38 @@ export interface RealHopperLargeFixtureOracle {
readonly stringCount: number;
}
/** Source-owned expected tokens for a fixture, grouped as nonempty string arrays. */
export type RealHopperFixtureExpectations = Readonly<
Record<string, readonly string[]>
>;
export interface RealHopperFixtureTargets {
readonly manifestPath: string;
readonly primary: RealHopperFixtureTarget;
readonly secondary: RealHopperFixtureTarget;
readonly large: RealHopperFixtureTarget;
readonly versionV1: RealHopperFixtureTarget;
readonly versionV2: RealHopperFixtureTarget;
readonly objc: RealHopperFixtureTarget;
readonly napi: RealHopperFixtureTarget;
readonly swift: RealHopperFixtureTarget | undefined;
readonly oracle: RealHopperFixtureOracle;
readonly largeOracle: RealHopperLargeFixtureOracle;
readonly versionV1Expectations: RealHopperFixtureExpectations;
readonly versionV2Expectations: RealHopperFixtureExpectations;
readonly objcExpectations: RealHopperFixtureExpectations;
readonly napiExpectations: RealHopperFixtureExpectations;
readonly swiftExpectations: RealHopperFixtureExpectations | undefined;
readonly compilers: Readonly<
Record<
string,
{
readonly path: string;
readonly version: string;
readonly arguments: readonly string[];
}
>
>;
}
export function loadRealHopperFixtureTargets(
+112 -16
View File
@@ -2,33 +2,104 @@ import { createHash } from "node:crypto";
import { readFile, realpath } from "node:fs/promises";
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
/** Bind the real-Hopper semantic verifier to source-owned manifest artifacts. */
const MANDATORY_FIXTURE_NAMES = [
"c",
"version-v1",
"version-v2",
"objc",
"napi",
"large",
];
const OPTIONAL_FIXTURE_NAMES = ["swift"];
/**
* Bind the real-Hopper semantic verifier to source-owned manifest artifacts.
*
* Every present artifact is distinct and digest-bound. Swift remains optional.
*/
export async function loadRealHopperFixtureTargets(manifestPath) {
const canonicalManifest = await realpath(manifestPath);
const manifest = JSON.parse(await readFile(canonicalManifest, "utf8"));
if (manifest?.schemaVersion !== 1 || !Array.isArray(manifest.fixtures))
throw new Error("Invalid conformance fixture manifest");
const primary = fixtureEntry(manifest.fixtures, "c");
const secondary = fixtureEntry(manifest.fixtures, "version-v2");
const large = fixtureEntry(manifest.fixtures, "large");
const oracle = requireCOracle(primary.expectations?.hopperOracle);
const largeOracle = requireLargeOracle(large.expectations);
const root = dirname(canonicalManifest);
const [primaryArtifact, secondaryArtifact, largeArtifact] = await Promise.all(
[
bindFixtureArtifact(root, primary),
bindFixtureArtifact(root, secondary),
bindFixtureArtifact(root, large),
],
const entries = new Map();
for (const name of MANDATORY_FIXTURE_NAMES)
entries.set(name, fixtureEntry(manifest.fixtures, name));
for (const name of OPTIONAL_FIXTURE_NAMES) {
const matches = manifest.fixtures.filter(
(fixture) => fixture?.name === name,
);
if (matches.length === 1) entries.set(name, matches[0]);
else if (matches.length > 1) throw new Error(`Duplicate fixture ${name}`);
}
const oracle = requireCOracle(entries.get("c")?.expectations?.hopperOracle);
const largeOracle = requireLargeOracle(entries.get("large")?.expectations);
const versionV1Expectations = requireStringArrayExpectations(
entries.get("version-v1"),
"version-v1",
);
const versionV2Expectations = requireStringArrayExpectations(
entries.get("version-v2"),
"version-v2",
);
const objcExpectations = requireStringArrayExpectations(
entries.get("objc"),
"objc",
);
const napiExpectations = requireStringArrayExpectations(
entries.get("napi"),
"napi",
);
const swiftExpectations = entries.has("swift")
? requireStringArrayExpectations(entries.get("swift"), "swift")
: undefined;
const bound = new Map();
for (const [name, entry] of entries)
bound.set(name, await bindFixtureArtifact(root, entry));
requireDistinctArtifacts([...bound.values()]);
const compilers = Object.fromEntries(
[...entries].map(([name, entry]) => [name, requireCompiler(entry, name)]),
);
requireDistinctArtifacts([primaryArtifact, secondaryArtifact, largeArtifact]);
return {
manifestPath: canonicalManifest,
primary: primaryArtifact,
secondary: secondaryArtifact,
large: largeArtifact,
primary: bound.get("c"),
secondary: bound.get("version-v2"),
large: bound.get("large"),
versionV1: bound.get("version-v1"),
versionV2: bound.get("version-v2"),
objc: bound.get("objc"),
napi: bound.get("napi"),
swift: bound.get("swift"),
oracle,
largeOracle,
versionV1Expectations,
versionV2Expectations,
objcExpectations,
napiExpectations,
swiftExpectations,
compilers,
};
}
function requireCompiler(entry, name) {
const compiler = entry?.compiler;
if (
compiler === null ||
typeof compiler !== "object" ||
typeof compiler.path !== "string" ||
compiler.path.length === 0 ||
typeof compiler.version !== "string" ||
compiler.version.length === 0 ||
!Array.isArray(compiler.arguments) ||
compiler.arguments.some((argument) => typeof argument !== "string")
)
throw new Error(`Fixture ${name} omitted its compiler provenance`);
return {
path: compiler.path,
version: compiler.version,
arguments: [...compiler.arguments],
};
}
@@ -73,6 +144,31 @@ function requireLargeOracle(largeOracle) {
return largeOracle;
}
function requireStringArrayExpectations(entry, name) {
const expectations = entry?.expectations;
if (
expectations === null ||
typeof expectations !== "object" ||
Array.isArray(expectations)
)
throw new Error(`Fixture ${name} omitted its expectations`);
const keys = Object.keys(expectations);
if (keys.length === 0)
throw new Error(`Fixture ${name} expectations must be nonempty`);
for (const key of keys) {
const value = expectations[key];
if (!Array.isArray(value) || value.length === 0)
throw new Error(`Fixture ${name} expectation ${key} must be nonempty`);
for (const item of value) {
if (typeof item !== "string" || item.length === 0)
throw new Error(
`Fixture ${name} expectation ${key} must be nonempty strings`,
);
}
}
return expectations;
}
function requireDistinctArtifacts(artifacts) {
if (
new Set(artifacts.map(({ path }) => path)).size !== artifacts.length ||
+28
View File
@@ -32,3 +32,31 @@ export function analyzeFixtureProcedure(
procedure: FixtureProcedure,
normalizedResult: (value: unknown, operation: string) => unknown,
): Promise<unknown>;
interface NamedFixtureOptions {
readonly client: ToolClient;
readonly options: unknown;
readonly target: { readonly path: string; readonly sha256: string };
readonly expectations: Readonly<Record<string, readonly string[]>>;
readonly normalizedResult: (value: unknown, operation: string) => any;
}
export function verifyNamedFixture(
options: NamedFixtureOptions,
): Promise<unknown>;
export function verifyLanguageFixture(
options: NamedFixtureOptions & {
readonly operations: readonly string[];
readonly semanticExpectations?: Readonly<Record<string, readonly string[]>>;
},
): Promise<unknown>;
export function verifyAbsentFixtureValues(options: {
readonly client: ToolClient;
readonly options: unknown;
readonly target: { readonly path: string; readonly sha256: string };
readonly symbols?: readonly string[];
readonly strings?: readonly string[];
readonly normalizedResult: (value: unknown, operation: string) => any;
}): Promise<void>;
+146
View File
@@ -89,6 +89,152 @@ export async function verifyRealHopperFixture({
};
}
/** Open one digest-bound fixture and prove every declared symbol and string. */
export async function verifyNamedFixture({
client,
options,
target,
expectations,
normalizedResult,
}) {
const opened = await client.callTool(
{ name: "open_binary", arguments: { path: target.path } },
options,
);
if (opened?.isError === true)
throw new Error(`open_binary failed for ${target.path}`);
const symbols = [];
for (const name of expectations.symbols ?? []) {
const result = normalizedResult(
await client.callTool(
{ name: "find_xrefs_to_name", arguments: { name } },
options,
),
`find_xrefs_to_name ${name}`,
);
if (
result?.status !== "resolved" ||
typeof result.name !== "string" ||
stripSymbolPrefix(result.name) !== stripSymbolPrefix(name)
)
throw new Error(`Hopper did not resolve expected symbol ${name}`);
symbols.push(result.name);
}
const strings = [];
for (const value of expectations.strings ?? []) {
const page = normalizedResult(
await client.callTool(
{
name: "search_strings",
arguments: {
pattern: value,
mode: "literal",
case_sensitive: true,
limit: 100,
},
},
options,
),
`search_strings ${value}`,
);
const matches = Array.isArray(page?.items)
? page.items.filter(
(item) => item?.value === value && item.value_truncated === false,
)
: [];
if (matches.length === 0)
throw new Error(`Hopper did not resolve expected string ${value}`);
strings.push(value);
}
return { path: target.path, sha256: target.sha256, symbols, strings };
}
/** Prove language-specific enhanced tools against their source-owned fixture. */
export async function verifyLanguageFixture({
client,
options,
target,
expectations,
operations,
semanticExpectations = {},
normalizedResult,
}) {
const inventory = await verifyNamedFixture({
client,
options,
target,
expectations,
normalizedResult,
});
const semantics = {};
for (const operation of operations) {
const result = normalizedResult(
await client.callTool({ name: operation, arguments: {} }, options),
operation,
);
if (result === null || typeof result !== "object")
throw new Error(`${operation} returned no semantic result`);
const serialized = JSON.stringify(result);
for (const expected of semanticExpectations[operation] ?? [])
if (!serialized.includes(expected))
throw new Error(`${operation} omitted expected semantic ${expected}`);
semantics[operation] = result;
}
return { ...inventory, semantics };
}
/** Prove values intentionally absent from one version fixture. */
export async function verifyAbsentFixtureValues({
client,
options,
target,
symbols = [],
strings = [],
normalizedResult,
}) {
const opened = await client.callTool(
{ name: "open_binary", arguments: { path: target.path } },
options,
);
if (opened?.isError === true)
throw new Error(`open_binary failed for ${target.path}`);
for (const name of symbols) {
const result = normalizedResult(
await client.callTool(
{ name: "find_xrefs_to_name", arguments: { name } },
options,
),
`find_xrefs_to_name ${name}`,
);
if (result?.status !== "unresolved")
throw new Error(`Hopper unexpectedly resolved symbol ${name}`);
}
for (const value of strings) {
const page = normalizedResult(
await client.callTool(
{
name: "search_strings",
arguments: {
pattern: value,
mode: "literal",
case_sensitive: true,
limit: 100,
},
},
options,
),
`search_strings ${value}`,
);
if (
Array.isArray(page?.items) &&
page.items.some((item) => item?.value === value)
)
throw new Error(`Hopper unexpectedly resolved string ${value}`);
}
}
const stripSymbolPrefix = (value) => value.replace(/^_+/u, "");
/** Reject malformed, unsafe, or error-level diagnostics from the MCP runtime. */
export function requireSafeDiagnostics(chunks) {
const lines = chunks
+96 -2
View File
@@ -18,6 +18,9 @@ import { loadRealHopperFixtureTargets } from "./lib/real-hopper-fixture.mjs";
import {
requireCurrentDocument,
requireSafeDiagnostics,
verifyAbsentFixtureValues,
verifyLanguageFixture,
verifyNamedFixture,
verifyRealHopperFixture,
} from "./lib/real-hopper-semantic.mjs";
import { openAndVerifyLargeFixture } from "./lib/real-hopper-pagination.mjs";
@@ -423,6 +426,74 @@ try {
"binary_overview after target switch",
);
const secondAnalysis = await verifyCurrentTarget(client, options);
const versionV1 = await verifyNamedFixture({
client,
options,
target: fixtureTargets.versionV1,
expectations: fixtureTargets.versionV1Expectations,
normalizedResult: requireSuccessfulTool,
});
const versionV2 = await verifyNamedFixture({
client,
options,
target: fixtureTargets.versionV2,
expectations: fixtureTargets.versionV2Expectations,
normalizedResult: requireSuccessfulTool,
});
await verifyAbsentFixtureValues({
client,
options,
target: fixtureTargets.versionV1,
symbols: ["_rea_added"],
strings: ["REA_VERSION_TWO"],
normalizedResult: requireSuccessfulTool,
});
await verifyAbsentFixtureValues({
client,
options,
target: fixtureTargets.versionV2,
strings: ["REA_VERSION_ONE"],
normalizedResult: requireSuccessfulTool,
});
const objc = await verifyLanguageFixture({
client,
options,
target: fixtureTargets.objc,
expectations: fixtureTargets.objcExpectations,
operations: ["get_objc_classes", "get_objc_protocols"],
semanticExpectations: {
get_objc_classes: ["REAWidget"],
get_objc_protocols: ["REAWidgetDelegate"],
},
normalizedResult: requireSuccessfulTool,
});
const napi = await verifyNamedFixture({
client,
options,
target: fixtureTargets.napi,
expectations: fixtureTargets.napiExpectations,
normalizedResult: requireSuccessfulTool,
});
const swift =
fixtureTargets.swift === undefined ||
fixtureTargets.swiftExpectations === undefined
? null
: await verifyLanguageFixture({
client,
options,
target: fixtureTargets.swift,
expectations: fixtureTargets.swiftExpectations,
operations: ["analyze_swift_types"],
semanticExpectations: {
analyze_swift_types: [
"REAService",
"REARecord",
"REAState",
"REAProtocol",
],
},
normalizedResult: requireSuccessfulTool,
});
const largePagination = await openAndVerifyLargeFixture({
client,
options,
@@ -462,15 +533,38 @@ try {
segmentCount: firstOverview.segment_count,
analyses: [firstAnalysis, secondAnalysis],
fixtureAnalysis,
versionFixtures: { versionV1, versionV2 },
languageFixtures: { objc, napi, swift },
largePagination,
fixtureManifest: fixtureTargets.manifestPath,
fixtureCompilers: fixtureTargets.compilers,
bundledMcpRunning,
stderrBytes,
diagnosticCount,
dynamicSession: true,
providerBinding,
targets: [targetA, targetB, largeTarget],
targetHashes: [targetHashA, targetHashB, fixtureTargets.large.sha256],
targets: [
targetA,
fixtureTargets.versionV1.path,
targetB,
fixtureTargets.objc.path,
fixtureTargets.napi.path,
...(fixtureTargets.swift === undefined
? []
: [fixtureTargets.swift.path]),
largeTarget,
],
targetHashes: [
targetHashA,
fixtureTargets.versionV1.sha256,
targetHashB,
fixtureTargets.objc.sha256,
fixtureTargets.napi.sha256,
...(fixtureTargets.swift === undefined
? []
: [fixtureTargets.swift.sha256]),
fixtureTargets.large.sha256,
],
switched: true,
secondOverview: verifiedSecondOverview,
};