mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 10:43:30 +00:00
feat: expand cross-platform investigation workflows
This commit is contained in:
1 parent
b537ebfa34
commit
dffc81c865
672 files changed
+13013
-2302
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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>;
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user