rea/scripts/lib/product-catalog.mjs

537 lines
17 KiB
JavaScript

import { pathToFileURL } from "node:url";
import { join } from "node:path";
import { Cli } from "incur";
import { format } from "prettier";
import { z } from "zod";
const load = (root, path) => import(pathToFileURL(join(root, path)).href);
const names = (contracts) =>
contracts
.map(({ name }) => name)
.sort((left, right) => left.localeCompare(right));
const valuesAt = (node, path) => {
if (typeof node !== "object" || node === null) return [];
if ("anyOf" in node && Array.isArray(node.anyOf))
return node.anyOf.flatMap((alternative) => valuesAt(alternative, path));
if (path.length === 0)
return "const" in node &&
(typeof node.const === "number" || typeof node.const === "string")
? [node.const]
: [];
if (
!("properties" in node) ||
typeof node.properties !== "object" ||
node.properties === null
)
return [];
const [segment, ...remaining] = path;
return segment !== undefined && segment in node.properties
? valuesAt(node.properties[segment], remaining)
: [];
};
const versionAt = (schema, path) => {
const versions = [
...new Set(valuesAt(z.toJSONSchema(schema), path).map(String)),
];
if (versions.length !== 1)
throw new Error(
`Schema version path ${path.join(".")} has ${String(versions.length)} constant values`,
);
const version = versions[0];
if (version === undefined) throw new Error("Schema version disappeared");
return /^\d+$/u.test(version) ? Number(version) : version;
};
/** Read primary names and aliases from the actual Incur command router. */
export const createCliInventory = (cli) => {
const commands = Cli.toCommands.get(cli);
if (commands === undefined)
throw new Error("Incur did not expose the registered REA CLI commands");
const primary = [];
const aliases = [];
for (const [name, entry] of commands) {
if ("_alias" in entry) aliases.push({ name, target: entry.target });
else primary.push(name);
}
return {
primary: primary.sort((left, right) => left.localeCompare(right)),
aliases: aliases.sort((left, right) => left.name.localeCompare(right.name)),
};
};
/** Read one primary command's declared option names from the Incur router. */
export const cliCommandOptionNames = (cli, name) => {
const commands = Cli.toCommands.get(cli);
const command = commands?.get(name);
if (
command === undefined ||
"_alias" in command ||
!("options" in command) ||
command.options === undefined
)
return [];
const schema = z.toJSONSchema(command.options);
return Object.keys(schema.properties ?? {}).sort((left, right) =>
left.localeCompare(right),
);
};
/** Report primary CLI fields whose generated JSON Schema lacks help text. */
export const cliCommandDescriptionIssues = (cli) => {
const commands = Cli.toCommands.get(cli);
if (commands === undefined)
throw new Error("Incur did not expose the registered REA CLI commands");
const missing = [];
for (const [commandName, command] of commands) {
if ("_alias" in command) continue;
for (const groupName of ["args", "options"]) {
if (!(groupName in command) || command[groupName] === undefined) continue;
const properties = z.toJSONSchema(command[groupName]).properties ?? {};
for (const [propertyName, property] of Object.entries(properties)) {
if (
typeof property !== "object" ||
property === null ||
typeof property.description !== "string" ||
property.description.trim() === ""
)
missing.push(`${commandName}.${groupName}.${propertyName}`);
}
}
}
return missing;
};
const assertSameNames = (label, actual, expected) => {
const actualSet = new Set(actual);
const expectedSet = new Set(expected);
const missing = [...expectedSet].filter((name) => !actualSet.has(name));
const extra = [...actualSet].filter((name) => !expectedSet.has(name));
if (missing.length === 0 && extra.length === 0) return;
throw new Error(
`${label} drifted (missing: ${missing.join(", ") || "none"}; extra: ${extra.join(", ") || "none"})`,
);
};
const SOURCE_PATHS = {
packageMetadata: "dist/generatedPackageMetadata.js",
catalogIdentity: "dist/catalogIdentity.js",
cli: "dist/cli.js",
toolContracts: "dist/contracts/toolContracts.js",
nativeContracts: "dist/contracts/nativeToolContracts.js",
artifactContracts: "dist/contracts/artifactToolContracts.js",
managedContracts: "dist/contracts/managedToolContracts.js",
managedWorkflowContracts: "dist/contracts/managedWorkflowToolContracts.js",
browserContracts: "dist/contracts/browserToolContracts.js",
electronContracts: "dist/contracts/electronToolContracts.js",
applicationContracts: "dist/contracts/applicationToolContracts.js",
supportedClients: "dist/application/SupportedClients.js",
hopperProvider: "dist/hopper/HopperProvider.js",
ghidraProvider: "dist/ghidra/GhidraProvider.js",
nativeProvider: "dist/native/NativeMacOSProvider.js",
artifactProviders: "dist/application/InvestigationProviders.js",
browserProvider: "dist/browser/CdpBrowserProvider.js",
electronProvider: "dist/browser/CdpElectronProvider.js",
evidence: "dist/domain/evidence.js",
evidenceBundle: "dist/domain/evidenceBundle.js",
processCapture: "dist/domain/processCapture.js",
analysisSnapshot: "dist/domain/analysisSnapshot.js",
artifactGraph: "dist/domain/artifactGraph.js",
investigationWorkspace: "dist/domain/investigationWorkspace.js",
browserObservation: "dist/domain/browserObservation.js",
browserSession: "dist/domain/browserSession.js",
electronObservation: "dist/domain/electronObservation.js",
webBundleAnalysis: "dist/domain/webBundleAnalysis.js",
webCaptureDiff: "dist/domain/webCaptureDiff.js",
managedArtifact: "dist/domain/managedArtifact.js",
managedComparison: "dist/domain/managedMemberComparison.js",
managedNativeVerification: "dist/domain/managedNativeVerification.js",
webMcpDiscovery: "dist/domain/webMcpDiscovery.js",
webScreenshot: "dist/domain/webScreenshot.js",
javascriptApplicationGraph: "dist/domain/javascriptApplicationGraph.js",
javascriptApplicationAnalysis: "dist/domain/javascriptApplicationAnalysis.js",
javascriptFeatureTrace: "dist/domain/javascriptFeatureTraceSchemas.js",
javascriptVersionComparison:
"dist/domain/javascriptApplicationVersionComparisonSchemas.js",
reconstructionVerification:
"dist/domain/reconstructionVerificationSchemas.js",
residualUnknown: "dist/domain/residualUnknown.js",
};
const loadSources = async (root) =>
Object.fromEntries(
await Promise.all(
Object.entries(SOURCE_PATHS).map(async ([key, path]) => [
key,
await load(root, path),
]),
),
);
const toolFamilyCatalog = (sources) => {
const families = [
{
id: "direct",
surface: "official-proxy",
contracts: sources.toolContracts.OFFICIAL_TOOL_CONTRACTS,
},
{
id: "enhanced",
surface: "enhanced",
contracts: sources.toolContracts.ENHANCED_TOOL_CONTRACTS,
},
{
id: "native",
surface: "native-provider",
contracts: sources.nativeContracts.NATIVE_TOOL_CONTRACTS,
},
{
id: "artifact",
surface: "artifact-provider",
contracts: sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
},
{
id: "managed",
surface: "managed",
contracts: [
...sources.managedContracts.MANAGED_TOOL_CONTRACTS,
...sources.managedWorkflowContracts.MANAGED_WORKFLOW_TOOL_CONTRACTS,
],
},
{
id: "browser",
surface: "browser-provider",
contracts: sources.browserContracts.BROWSER_TOOL_CONTRACTS,
},
{
id: "electron",
surface: "electron-provider",
contracts: sources.electronContracts.ELECTRON_TOOL_CONTRACTS,
},
{
id: "application",
surface: "application-workflow",
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
},
{
id: "session",
surface: "session",
contracts: sources.toolContracts.SESSION_TOOL_CONTRACTS,
},
].map(({ id, surface, contracts }) => ({
id,
surface,
count: contracts.length,
tools: names(contracts),
}));
const familyTotal = families.reduce(
(total, family) => total + family.count,
0,
);
if (
familyTotal !== sources.toolContracts.TOOL_CONTRACTS.length ||
familyTotal !== sources.catalogIdentity.CATALOG_IDENTITY.counts.mcp_tools
)
throw new Error("Tool family inventory does not match TOOL_CONTRACTS");
return { total: familyTotal, families };
};
const providerCatalog = (sources) => {
const applicationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "analyze_javascript_application",
);
const reconciliationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "reconcile_javascript_runtime",
);
const observationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) =>
name !== "analyze_javascript_application" &&
name !== "reconcile_javascript_runtime",
);
if (
applicationContracts.length !== 1 ||
reconciliationContracts.length !== 1 ||
observationContracts.length !== 2
)
throw new Error("Electron provider capability ownership drifted");
return [
{
identity: sources.hopperProvider.HOPPER_PROVIDER_IDENTITY,
contracts: sources.hopperProvider.HOPPER_PROVIDER_TOOL_CONTRACTS,
},
{
identity: sources.ghidraProvider.GHIDRA_PROVIDER_IDENTITY,
contracts: sources.ghidraProvider.GHIDRA_PROVIDER_TOOL_CONTRACTS,
},
{
identity: sources.nativeProvider.NATIVE_MACOS_PROVIDER_IDENTITY,
contracts: sources.nativeContracts.NATIVE_TOOL_CONTRACTS,
},
{
identity: sources.artifactProviders.ARTIFACT_GRAPH_PROVIDER,
contracts: sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
},
{
identity: sources.artifactProviders.MANAGED_STATIC_PROVIDER,
contracts: sources.managedContracts.MANAGED_TOOL_CONTRACTS,
},
{
identity: sources.artifactProviders.MANAGED_WORKFLOW_PROVIDER,
contracts:
sources.managedWorkflowContracts.MANAGED_WORKFLOW_TOOL_CONTRACTS,
},
{
identity: sources.browserProvider.CDP_BROWSER_PROVIDER_IDENTITY,
contracts: sources.browserContracts.BROWSER_TOOL_CONTRACTS,
},
{
identity: sources.electronProvider.CDP_ELECTRON_PROVIDER_IDENTITY,
contracts: observationContracts,
},
{
identity: sources.artifactProviders.JAVASCRIPT_APPLICATION_PROVIDER,
contracts: applicationContracts,
},
{
identity:
sources.artifactProviders.JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER,
contracts: reconciliationContracts,
},
{
identity:
sources.artifactProviders.JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER,
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
},
]
.map(({ identity, contracts }) => ({
id: identity.id,
name: identity.name,
version: identity.version,
capabilities: names(contracts),
}))
.sort((left, right) => left.id.localeCompare(right.id));
};
const durableSchemaDefinitions = (sources) => [
[
"analysis_snapshot",
sources.analysisSnapshot.analysisSnapshotSchema,
["snapshot_version"],
],
[
"artifact_graph",
sources.artifactGraph.artifactInventoryResultSchema,
["manifest", "schema_version"],
],
[
"artifact_extraction",
sources.artifactGraph.artifactExtractionResultSchema,
["extraction_manifest", "schema_version"],
],
["evidence", sources.evidence.evidenceSchema, ["schema_version"]],
[
"evidence_bundle",
sources.evidenceBundle.evidenceBundleSchema,
["bundle_version"],
],
[
"investigation_run",
sources.investigationWorkspace.investigationRunSchema,
["schema_version"],
],
[
"investigation_run_summary",
sources.investigationWorkspace.investigationRunSummarySchema,
["schema_version"],
],
[
"investigation_workspace",
sources.investigationWorkspace.investigationWorkspaceSchema,
["workspace_version"],
],
[
"javascript_application_graph",
sources.javascriptApplicationGraph.javascriptApplicationGraphSchema,
["schema_version"],
],
[
"javascript_application_version_comparison",
sources.javascriptVersionComparison
.applicationVersionComparisonResultSchema,
["schema_version"],
],
[
"javascript_feature_trace",
sources.javascriptFeatureTrace.applicationFeatureTraceResultSchema,
["schema_version"],
],
[
"process_capture",
sources.processCapture.processCaptureSchema,
["schema_version"],
],
[
"reconstruction_verification",
sources.reconstructionVerification.reconstructionSpecificationSchema,
["schema_version"],
],
[
"residual_unknown",
sources.residualUnknown.residualUnknownSchema,
["registry_version"],
],
];
const observationSchemaDefinitions = (sources) => [
[
"managed_artifact_inspection",
sources.managedArtifact.managedArtifactInspectionSchema,
["schema_version"],
],
[
"managed_member_inspection",
sources.managedArtifact.managedMemberInspectionSchema,
["schema_version"],
],
[
"managed_native_boundary_inspection",
sources.managedArtifact.managedNativeBoundaryInspectionSchema,
["schema_version"],
],
[
"managed_member_comparison",
sources.managedComparison.managedMemberComparisonResultSchema,
["schema_version"],
],
[
"managed_native_verification",
sources.managedNativeVerification.managedNativeVerificationResultSchema,
["schema_version"],
],
[
"browser_target_list",
sources.browserObservation.browserTargetListSchema,
["schema_version"],
],
[
"electron_page_inspection",
sources.electronObservation.electronPageInspectionSchema,
["schema_version"],
],
[
"electron_target_list",
sources.electronObservation.electronTargetListSchema,
["schema_version"],
],
[
"javascript_application_analysis",
sources.javascriptApplicationAnalysis
.javascriptApplicationAnalysisResultSchema,
["schema_version"],
],
[
"web_bundle_analysis",
sources.webBundleAnalysis.webBundleAnalysisSchema,
["schema_version"],
],
[
"web_capture_diff",
sources.webCaptureDiff.webCaptureDiffSchema,
["schema_version"],
],
[
"web_mcp_discovery",
sources.webMcpDiscovery.webMcpDiscoverySchema,
["schema_version"],
],
[
"web_observation_session",
sources.browserSession.webObservationSessionSchema,
["schema_version"],
],
[
"web_page_inspection",
sources.browserObservation.webPageInspectionSchema,
["schema_version"],
],
[
"web_screenshot",
sources.webScreenshot.webScreenshotSchema,
["schema_version"],
],
[
"web_screenshot_diff",
sources.webScreenshot.webScreenshotDiffSchema,
["schema_version"],
],
];
const schemaCatalog = (sources) =>
[
...durableSchemaDefinitions(sources),
...observationSchemaDefinitions(sources),
]
.map(([id, schema, path]) => {
try {
return { id, version: versionAt(schema, path) };
} catch (cause) {
throw new Error(`Could not derive ${id} schema version`, { cause });
}
})
.sort((left, right) => left.id.localeCompare(right.id));
/** Project current runtime contracts into deterministic, machine-readable facts. */
export const createProductCatalog = async (root) => {
const sources = await loadSources(root);
const cli = createCliInventory(sources.cli.createCli());
assertSameNames(
"Primary CLI inventory",
cli.primary,
sources.catalogIdentity.CLI_COMMAND_NAMES,
);
const tools = toolFamilyCatalog(sources);
const metadata = sources.packageMetadata.PACKAGE_METADATA;
return {
catalog_schema_version: 1,
package: {
name: metadata.name,
version: metadata.version,
sdk: {
server: metadata.serverSdkVersion,
client: metadata.clientSdkVersion,
core: metadata.coreSdkVersion,
},
skill_version: metadata.skillVersion,
},
tools,
providers: providerCatalog(sources),
setup_clients: sources.supportedClients.SUPPORTED_CLIENT_DEFINITIONS.map(
({ name, displayName, format }) => ({
id: name,
display_name: displayName,
format,
configuration: format === "unsupported" ? "detect-only" : "managed",
}),
),
schemas: schemaCatalog(sources),
cli: {
primary_count: cli.primary.length,
commands: cli.primary,
aliases: cli.aliases,
},
runtime_catalog: {
counts: sources.catalogIdentity.CATALOG_IDENTITY.counts,
digests: sources.catalogIdentity.CATALOG_IDENTITY.digests,
},
};
};
/** Stable checked-in representation of the product catalog. */
export const serializeProductCatalog = (catalog) =>
format(JSON.stringify(catalog, null, 2), { parser: "json" });