refactor(cli): project view-delivery inspectors from their contracts (#10600)

* refactor(cli): project view-delivery inspectors from their contracts

* refactor(cli): project APK signature, desktop application and EVM inspectors from contracts (#10603)
This commit is contained in:
morluto authored and GitHub committed 2026-11-15 15:00:24 +08:00
1 parent 254bb21857
commit fa53f7070a
15 files changed
+622 -605

No files matched your search

+5 -2
View File
@@ -608,8 +608,11 @@ and rendered for the CLI framework by `registerContractCommand` in
The projection names only what the CLI spells differently: positionals,
caller-relative path fields, option renames and short flags, CLI help wording,
omitted optional fields with a reason, Evidence delivery options, and whether the contract's cross-field
rules refuse at argv parsing or at the service's contract parse. Field schemas
and decimal argv tokens are derived, and
rules refuse at argv parsing or at the service's contract parse. A delivery
`view` is JSON text decoded when the command runs; the contract input then
admits the view with its `detail` before any target is opened, including a
`detail`-discriminated union input. Field schemas and decimal argv tokens are
derived, and
`tests/boundary/cli/cliContractCommandParity.test.ts` checks that each projected
command admits what its contract admits.
+36 -74
View File
@@ -1,20 +1,12 @@
import { parseCliJsonOption } from "./cliJsonOption.js";
import { z } from "incur";
import type { Logger } from "pino";
import { APK_SIGNATURE_PROVIDER_IDENTITY } from "../apkSignature/ApkSignatureRelease.js";
import { apkSignatureModule } from "../composition/apkSignature.js";
import { inspectApkSignatureInputSchema } from "../domain/apkSignature/apkSignatureQuery.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { logCliCommand } from "../cliLogging.js";
import { localPathStringSchema } from "../domain/localPath.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
import {
analysisEvidenceDeliveryOptions,
deliverCliAnalysisEvidence,
} from "./analysisEvidenceDelivery.js";
deliverWithService,
registerContractCommand,
} from "./contractCommand.js";
import type { CliInstance } from "./types.js";
/** CLI shares selected delivery and complete-Evidence publication with other artifact tools. */
export const registerApkSignatureCommands = (
@@ -22,65 +14,35 @@ export const registerApkSignatureCommands = (
logger: Logger,
environment: Readonly<NodeJS.ProcessEnv>,
): void => {
cli.command(apkSignatureModule.cliCommand, {
description:
"Inspect declared APK signing schemes, signers and content digests",
args: z.object({
path: localPathStringSchema.describe("Selected local APK/ZIP/AAB"),
}),
options: z.object({
...analysisEvidenceDeliveryOptions,
view: z
.string()
.optional()
.describe(
"JSON signer/digest/lineage selection; requires --detail summary and --evidence_output",
),
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, apkSignatureModule.cliCommand, async () => {
const decoded = parseCliJsonOption(
"inspect_apk_signature",
"view",
options.view,
"view must be a JSON APK signature selection",
);
if (!decoded.ok) return projectAnalysisError(decoded.error);
const input = inspectApkSignatureInputSchema.safeParse({
path: absoluteCallerPath(args.path),
detail: options.detail,
...(decoded.value === undefined ? {} : { view: decoded.value }),
});
if (!input.success)
return projectAnalysisError(
analysisInputErrorFromIssues(
"inspect_apk_signature",
input.error.issues,
{ view: decoded.value },
),
);
const service = apkSignatureModule.create(environment);
return deliverCliAnalysisEvidence({
callerInputPath: args.path,
run: () =>
service.inspect(
{ path: input.data.path, detail: "complete" },
{ signal },
),
service,
providerId: APK_SIGNATURE_PROVIDER_IDENTITY.id,
operation: apkSignatureModule.contract.name,
signal,
detail: options.detail,
evidenceOutput: options.evidence_output,
...(input.data.view === undefined
? {}
: {
view: { kind: "apk-signature" as const, ...input.data.view },
}),
});
}),
),
});
const deliver = deliverWithService(
() => apkSignatureModule.create(environment),
APK_SIGNATURE_PROVIDER_IDENTITY.id,
);
registerContractCommand(
cli,
logger,
apkSignatureModule.contract,
{
command: apkSignatureModule.cliCommand,
description:
"Inspect declared APK signing schemes, signers and content digests",
positionals: ["path"],
callerPaths: ["path"],
evidenceDelivery: true,
viewDelivery: {
invalidJson: "view must be a JSON APK signature selection",
kind: "apk-signature",
},
fields: {
path: { description: "Selected local APK/ZIP/AAB" },
view: {
description:
"JSON signer/digest/lineage selection; requires --detail summary and --evidence_output",
},
},
},
// The CLI delivers the summary itself, so the service returns complete
// Evidence; the contract's own default would retain a summary instead.
(input, context) => deliver({ ...input, detail: "complete" }, context),
);
};
+3 -4
View File
@@ -30,6 +30,7 @@ import {
inspectWithService,
registerContractCommand,
} from "./contractCommand.js";
import { PRODUCER_LIMITS_HELP } from "./producerLimitsOption.js";
type BinaryDiagnosticsOperation =
(typeof BINARY_DIAGNOSTICS_TOOL_CONTRACTS)[number]["name"];
@@ -309,8 +310,6 @@ export const registerBinaryDiagnosticsCommands = (
},
deliverWithService(createNativeExportsService, "rea-native-exports"),
);
const producerLimitsHelp =
"JSON producer budget overrides. Keys and units follow this operation's producer_limits contract; changes apply on this call without restart and preserve tighter inherited OS limits.";
const layout = createBinaryLayoutService(environment);
registerContractCommand(
cli,
@@ -325,7 +324,7 @@ export const registerBinaryDiagnosticsCommands = (
jsonText: ["producer_limits"],
fields: {
path: { description: "Local ELF binary or relocatable object" },
producer_limits: { description: producerLimitsHelp },
producer_limits: { description: PRODUCER_LIMITS_HELP },
},
omit: {
detail:
@@ -355,7 +354,7 @@ export const registerBinaryDiagnosticsCommands = (
path: {
description: "Local Linux ELF64 amd64 or AArch64 little-endian core",
},
producer_limits: { description: producerLimitsHelp },
producer_limits: { description: PRODUCER_LIMITS_HELP },
include_debugger_context: {
name: "debuggerContext",
description: "Add BYO core-only GDB/pwndbg mapping candidates",
+195 -20
View File
@@ -1,8 +1,16 @@
import { z } from "zod";
import type { AnalysisInputError } from "../domain/analysisErrorCore.js";
import type { AnalysisViewRequest } from "../domain/analysisView/analysisViewRequest.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { localPathStringSchema } from "../domain/localPath.js";
import { err, ok, type Result } from "../domain/result.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { analysisEvidenceDeliveryOptions } from "./analysisEvidenceDelivery.js";
import {
analysisEvidenceDeliveryOptions,
cliEvidenceDeliveryInputError,
} from "./analysisEvidenceDelivery.js";
import { parseCliJsonOption } from "./cliJsonOption.js";
import { safeParseJson } from "../domain/safeJson.js";
import { cliDecimalShape } from "./options.js";
@@ -13,10 +21,16 @@ import { cliDecimalShape } from "./options.js";
* framework adapter (today `contractCommand.ts` for Incur) only renders it.
*/
/** The parts of a tool contract that a CLI projection reads. */
/**
* The parts of a tool contract that a CLI projection reads. A union input
* shares every field except those the CLI delivery owns, such as a
* `detail`-discriminated delivery.
*/
export interface ProjectedContract<Name extends string = string> {
readonly name: Name;
readonly inputSchema: z.ZodObject;
readonly inputSchema:
| z.ZodObject
| z.ZodDiscriminatedUnion<readonly z.ZodObject[]>;
}
/** A contract input field name. */
@@ -74,6 +88,26 @@ export interface CliContractProjection<Field extends string> {
* field selects that delivery and is not passed to the analysis.
*/
readonly evidenceDelivery?: true;
/**
* With `evidenceDelivery`, the contract `view` field selects the delivered
* page. `--view` takes JSON text that is decoded when the command runs, and
* the delivery is then checked against the contract input before any
* target is opened.
*/
readonly viewDelivery?: CliViewDelivery;
}
/** How a contract command takes its `view` selection. */
export interface CliViewDelivery {
/** The refusal when `--view` is not JSON. */
readonly invalidJson: string;
/** Added to the selection when the contract's view does not name its kind. */
readonly kind?: AnalysisViewRequest["kind"];
/**
* `--detail` has no default: a view selects summary delivery, and complete
* applies otherwise, as in the contract.
*/
readonly viewSelectsSummary?: true;
}
/** A CLI option with no contract field, such as `--evidence_output`. */
@@ -87,6 +121,8 @@ export interface CliOnlyOption {
export interface CliEvidenceDeliveryChoice {
readonly detail: "complete" | "summary";
readonly evidenceOutput: string | undefined;
/** The admitted view, for a projection with `viewDelivery`. */
readonly view?: AnalysisViewRequest;
}
/** One positional argument or option of a contract command. */
@@ -115,6 +151,11 @@ export interface ContractCliParameter {
readonly jsonText: boolean;
/** Selects CLI Evidence delivery and is not analysis input. */
readonly delivery: boolean;
/**
* A delivery view as JSON text, decoded when the command runs and admitted
* with the delivery by the contract input.
*/
readonly viewSelection: boolean;
}
/** One cross-field refusal, naming CLI keys. */
@@ -129,6 +170,11 @@ export interface ContractCliDescription {
readonly command: string;
readonly description: string;
readonly contract: ProjectedContract;
/**
* Each contract input field's own schema. In a union input, a field the
* delivery owns admits the value of any branch.
*/
readonly fields: Readonly<Record<string, z.ZodType>>;
readonly parameters: readonly ContractCliParameter[];
/** Options after the contract parameters that no contract field owns. */
readonly cliOnlyOptions: readonly CliOnlyOption[];
@@ -153,10 +199,15 @@ export interface ContractCliDescription {
readonly callerInputs: (
values: Readonly<Record<string, unknown>>,
) => Readonly<Record<string, string>>;
/** The selected delivery, for a projection with `evidenceDelivery`. */
readonly delivery: (
values: Readonly<Record<string, unknown>>,
) => CliEvidenceDeliveryChoice;
/**
* The selected delivery, for a projection with `evidenceDelivery`, refused
* before any target is opened when it is not admitted.
*/
readonly delivery:
| ((
values: Readonly<Record<string, unknown>>,
) => Result<CliEvidenceDeliveryChoice, AnalysisInputError>)
| undefined;
}
/** One contract from a contract group, keeping its exact schema types. */
@@ -310,16 +361,24 @@ const renameConstraint = (
);
};
/** The union keyword naming a root discriminator, checked with the delivery. */
const ROOT_DISCRIMINATOR = "x-rea-root-discriminator";
const projectObjectConstraints = (
contract: ProjectedContract,
rename: (field: string) => string,
deliveryFields: ReadonlySet<string>,
): Record<string, unknown> | undefined => {
const metadata = contract.inputSchema.meta();
if (metadata === undefined) return undefined;
for (const keyword of Object.keys(metadata))
if (
!ROOT_ANNOTATIONS.has(keyword) &&
!(OBJECT_CONSTRAINTS as readonly string[]).includes(keyword)
!(OBJECT_CONSTRAINTS as readonly string[]).includes(keyword) &&
!(
keyword === ROOT_DISCRIMINATOR &&
deliveryFields.has(String(metadata[keyword]))
)
)
throw new Error(
`${contract.name} input keyword ${keyword} has no CLI projection`,
@@ -340,12 +399,59 @@ const projectObjectConstraints = (
return Object.keys(projected).length === 0 ? undefined : projected;
};
/**
* Each input field's schema. Branches of a union input must share each field
* the CLI takes as an argument; only delivery-owned fields may differ, and
* they admit any branch's value here. The delivery is then checked against
* the whole union.
*/
const inputFields = (
contract: ProjectedContract,
deliveryFields: ReadonlySet<string>,
): Record<string, z.ZodType> => {
const schema = contract.inputSchema;
if (schema instanceof z.ZodObject)
return schema.shape as Record<string, z.ZodType>;
const shapes = schema.options.map(
(option) => option.shape as Record<string, z.ZodType>,
);
const names = new Set(shapes.flatMap((shape) => Object.keys(shape)));
return Object.fromEntries(
[...names].map((field) => {
const branches = shapes.map((shape) => shape[field]);
const [first] = branches;
if (branches.every((branch) => branch === first)) return [field, first!];
if (
!deliveryFields.has(field) ||
branches.some((branch) => branch === undefined)
)
throw new Error(
`${contract.name}.${field} differs between input branches and has no argv projection`,
);
return [field, z.union(branches as z.ZodType[])];
}),
);
};
/** Derive the CLI parameters of one contract from its input schema and projection. */
export const describeContractCommand = <Contract extends ProjectedContract>(
contract: Contract,
projection: CliContractProjection<ContractField<Contract>>,
): ContractCliDescription => {
const shape = contract.inputSchema.shape as Record<string, z.ZodType>;
const evidenceDelivery = projection.evidenceDelivery === true;
const { viewDelivery } = projection;
if (viewDelivery !== undefined && !evidenceDelivery)
throw new Error(`${contract.name} view delivery requires evidenceDelivery`);
const deliveryFields: ReadonlySet<string> = new Set(
evidenceDelivery
? viewDelivery === undefined
? ["detail"]
: ["detail", "view"]
: [],
);
const shape = inputFields(contract, deliveryFields);
if (viewDelivery !== undefined && !admitsOmission(shape.view ?? z.never()))
throw new Error(`${contract.name} has no optional input field view`);
const positionals: readonly string[] = projection.positionals ?? [];
const callerPaths: ReadonlySet<string> = new Set(
projection.callerPaths ?? [],
@@ -374,16 +480,21 @@ export const describeContractCommand = <Contract extends ProjectedContract>(
if (!admitsOmission(shape[field]!))
throw new Error(`${contract.name}.${field} is required on the CLI`);
const evidenceDelivery = projection.evidenceDelivery === true;
const parameter = (field: string): ContractCliParameter => {
const declared = shape[field]!;
const projected = fieldProjections[field];
const callerPath = callerPaths.has(field);
const delivery = evidenceDelivery && field === "detail";
const delivery = deliveryFields.has(field);
const viewSelection = delivery && field === "view";
const jsonText = jsonTextFields.has(field);
const key = projected?.name ?? camelCase(field);
let schema: z.ZodType;
if (delivery) schema = analysisEvidenceDeliveryOptions.detail;
if (viewSelection) schema = z.string().optional();
else if (delivery)
schema =
viewDelivery?.viewSelectsSummary === true
? analysisEvidenceDeliveryOptions.detail.unwrap().optional()
: analysisEvidenceDeliveryOptions.detail;
else if (jsonText) schema = jsonTextField(declared, kebabCase(key));
else if (callerPath) {
schema = admitsOmission(declared)
@@ -412,6 +523,7 @@ export const describeContractCommand = <Contract extends ProjectedContract>(
callerPath,
delivery,
jsonText,
viewSelection,
};
};
const parameters = [
@@ -494,7 +606,12 @@ export const describeContractCommand = <Contract extends ProjectedContract>(
parameters,
cliOnlyOptions,
omitted,
optionConstraints: projectObjectConstraints(contract, rename),
fields: shape,
optionConstraints: projectObjectConstraints(
contract,
rename,
deliveryFields,
),
checkOptions,
toContractInput,
callerInputs: (values) =>
@@ -506,12 +623,70 @@ export const describeContractCommand = <Contract extends ProjectedContract>(
: [];
}),
),
delivery: (values) => ({
detail: values.detail === "summary" ? "summary" : "complete",
evidenceOutput:
typeof values.evidence_output === "string"
? values.evidence_output
: undefined,
}),
delivery: evidenceDelivery
? (values) => {
const selected = selectDelivery(
contract,
viewDelivery,
values,
toContractInput,
rename,
);
if (!selected.ok) return selected;
const refused = cliEvidenceDeliveryInputError({
...selected.value,
operation: contract.name,
});
return refused === undefined ? selected : err(refused);
}
: undefined,
};
};
/**
* The delivery the parsed values select. A view is decoded from its JSON
* text and admitted with the delivery and the other inputs by the contract
* input schema, so the contract's own delivery rules and messages apply.
*/
const selectDelivery = (
contract: ProjectedContract,
viewDelivery: CliViewDelivery | undefined,
values: Readonly<Record<string, unknown>>,
toContractInput: ContractCliDescription["toContractInput"],
rename: (field: string) => string,
): Result<CliEvidenceDeliveryChoice, AnalysisInputError> => {
const detail = values[rename("detail")] as "complete" | "summary" | undefined;
const evidenceOutput = values.evidence_output as string | undefined;
if (viewDelivery === undefined)
return ok({ detail: detail ?? "complete", evidenceOutput });
const viewKey = rename("view");
const decoded = parseCliJsonOption(
contract.name,
viewKey,
values[viewKey] as string | undefined,
viewDelivery.invalidJson,
);
if (!decoded.ok) return decoded;
const input = {
...toContractInput(values),
...(detail === undefined ? {} : { detail }),
...(decoded.value === undefined ? {} : { view: decoded.value }),
};
const admitted = contract.inputSchema.safeParse(input);
if (!admitted.success)
return err(
analysisInputErrorFromIssues(contract.name, admitted.error.issues, input),
);
const { view } = admitted.data as { readonly view?: object };
return ok({
detail: detail ?? (view === undefined ? "complete" : "summary"),
evidenceOutput,
...(view === undefined
? {}
: {
view: (viewDelivery.kind === undefined
? view
: { kind: viewDelivery.kind, ...view }) as AnalysisViewRequest,
}),
});
};
+17 -9
View File
@@ -3,6 +3,7 @@ import type { Logger } from "pino";
import { logCliCommand } from "../cliLogging.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { Evidence } from "../domain/evidence.js";
import type { Result } from "../domain/result.js";
import { deliverCliAnalysisEvidence } from "./analysisEvidenceDelivery.js";
@@ -32,8 +33,8 @@ export interface ContractCommandContext {
readonly operation: string;
/** Each caller-path field as the caller spelled it, before resolution. */
readonly callerInputs: Readonly<Record<string, string>>;
/** The selected delivery, for a projection with `evidenceDelivery`. */
readonly delivery: CliEvidenceDeliveryChoice;
/** The admitted delivery, for a projection with `evidenceDelivery`. */
readonly delivery: CliEvidenceDeliveryChoice | undefined;
}
/** A closeable one-shot service that inspects one contract input. */
@@ -82,6 +83,9 @@ export const deliverWithService =
(service: () => EvidenceInspectionService, providerId: string) =>
(input: unknown, context: ContractCommandContext) => {
const callerInputPath = primaryCallerPath(context);
const { delivery } = context;
if (delivery === undefined)
throw new Error(`${context.operation} does not declare evidenceDelivery`);
const owned = service();
return deliverCliAnalysisEvidence({
callerInputPath,
@@ -90,8 +94,9 @@ export const deliverWithService =
providerId,
operation: context.operation,
signal: context.signal,
detail: context.delivery.detail,
evidenceOutput: context.delivery.evidenceOutput,
detail: delivery.detail,
evidenceOutput: delivery.evidenceOutput,
...(delivery.view === undefined ? {} : { view: delivery.view }),
});
};
@@ -174,8 +179,11 @@ export const registerContractCommand = <
}) => {
const values = { ...options, ...args };
return withCommandCancellation((signal) =>
logCliCommand(logger, description.command, () =>
run(
logCliCommand(logger, description.command, async () => {
const delivery = description.delivery?.(values);
if (delivery?.ok === false)
return projectAnalysisError(delivery.error);
return run(
description.toContractInput(values) as z.input<
Contract["inputSchema"]
>,
@@ -183,10 +191,10 @@ export const registerContractCommand = <
signal,
operation: contract.name,
callerInputs: description.callerInputs(values),
delivery: description.delivery(values),
delivery: delivery?.value,
},
),
),
);
}),
);
},
} as never);
+38 -58
View File
@@ -1,67 +1,47 @@
import { localPathStringSchema } from "../domain/localPath.js";
import { inspectDesktopApplicationInputSchema } from "../domain/desktopApplication.js";
import {
analysisEvidenceDeliveryOptions,
deliverCliAnalysisEvidence,
} from "./analysisEvidenceDelivery.js";
import { z } from "incur";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { createDesktopApplicationService } from "../composition/desktop.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { cliDecimalField } from "./options.js";
import { createDesktopApplicationService } from "../composition/desktop.js";
import { DESKTOP_APPLICATION_TOOL_CONTRACTS } from "../contracts/desktopApplicationToolContracts.js";
import { contractNamed } from "./contractCliDescription.js";
import {
deliverWithService,
registerContractCommand,
} from "./contractCommand.js";
import type { CliInstance } from "./types.js";
/** One-shot complete Evidence can be saved and queried with inspect-analysis-view. */
export const registerDesktopApplicationCommands = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.inspectDesktopApplication, {
description:
"Identify desktop runtime candidates from bounded directory and header observations",
args: z.object({
path: localPathStringSchema.describe("Local installation directory"),
}),
options: z.object({
// Decimal tokens only: numeric coercion would read "0x1" as 1 and
// "1e1" as 10. The shared fields keep their bounds and defaults.
max_entries: cliDecimalField(
inspectDesktopApplicationInputSchema.shape.max_entries,
).describe(
"Maximum directory entries to examine; exhaustion leaves partial coverage",
),
...analysisEvidenceDeliveryOptions,
max_probe_bytes: cliDecimalField(
inspectDesktopApplicationInputSchema.shape.max_probe_bytes,
).describe("Maximum total bytes read from selected file ranges"),
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(
logger,
CLI_COMMANDS.inspectDesktopApplication,
async () => {
const { detail, evidence_output, ...limits } = options;
const service = createDesktopApplicationService();
return deliverCliAnalysisEvidence({
callerInputPath: args.path,
run: () =>
service.inspect(
{ path: absoluteCallerPath(args.path), ...limits },
{ signal },
),
service,
providerId: "rea-artifact-graph",
operation: "inspect_desktop_application",
signal,
detail,
evidenceOutput: evidence_output,
});
},
),
),
});
registerContractCommand(
cli,
logger,
contractNamed(
DESKTOP_APPLICATION_TOOL_CONTRACTS,
"inspect_desktop_application",
),
{
command: CLI_COMMANDS.inspectDesktopApplication,
description:
"Identify desktop runtime candidates from bounded directory and header observations",
positionals: ["path"],
callerPaths: ["path"],
evidenceDelivery: true,
fields: {
path: { description: "Local installation directory" },
max_entries: {
name: "max_entries",
description:
"Maximum directory entries to examine; exhaustion leaves partial coverage",
},
max_probe_bytes: {
name: "max_probe_bytes",
description: "Maximum total bytes read from selected file ranges",
},
},
},
deliverWithService(createDesktopApplicationService, "rea-artifact-graph"),
);
};
+31 -72
View File
@@ -1,81 +1,40 @@
import { z } from "incur";
import { localPathStringSchema } from "../domain/localPath.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { createBeamService } from "../composition/erlang.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { inspectErlangBeamInputSchema } from "../domain/erlang/beam.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { createBeamService } from "../composition/erlang.js";
import { ERLANG_BEAM_TOOL_CONTRACTS } from "../contracts/erlang/beamToolContracts.js";
import { contractNamed } from "./contractCliDescription.js";
import {
analysisEvidenceDeliveryOptions,
deliverCliAnalysisEvidence,
parseCliAnalysisEvidenceDelivery,
} from "./analysisEvidenceDelivery.js";
import { erlangBeamDeliverySchema } from "../domain/analysisView/erlangBeamView.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { cliDecimalField } from "./options.js";
deliverWithService,
registerContractCommand,
} from "./contractCommand.js";
import type { CliInstance } from "./types.js";
/** Inspect operator-selected BEAM files through the same shared workflow as MCP. */
export const registerErlangCommands = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.inspectErlangBeam, {
description:
"Inspect Erlang BEAM atoms, exports, imports and Code header without executing bytecode",
args: z.object({
path: localPathStringSchema.describe("Local BEAM file"),
}),
options: z.object({
// Decimal tokens only, as for inspect-python-pyc.
maxFileBytes: cliDecimalField(
inspectErlangBeamInputSchema.shape.max_file_bytes,
),
maxChunks: cliDecimalField(inspectErlangBeamInputSchema.shape.max_chunks),
maxAtoms: cliDecimalField(inspectErlangBeamInputSchema.shape.max_atoms),
...analysisEvidenceDeliveryOptions,
view: z
.string()
.optional()
.describe(
"JSON beam view; requires --detail summary and --evidence_output",
),
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectErlangBeam, async () => {
const delivery = parseCliAnalysisEvidenceDelivery(
"inspect_erlang_beam",
options,
erlangBeamDeliverySchema,
);
if (!delivery.ok) return projectAnalysisError(delivery.error);
const service = createBeamService();
return deliverCliAnalysisEvidence({
callerInputPath: args.path,
run: () =>
service.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_chunks: options.maxChunks,
max_atoms: options.maxAtoms,
},
{ signal },
),
service,
providerId: "rea-erlang-beam",
operation: "inspect_erlang_beam",
signal,
detail: delivery.value.detail,
evidenceOutput: delivery.value.evidenceOutput,
...(delivery.value.view === undefined
? {}
: { view: delivery.value.view }),
});
}),
),
});
registerContractCommand(
cli,
logger,
contractNamed(ERLANG_BEAM_TOOL_CONTRACTS, "inspect_erlang_beam"),
{
command: CLI_COMMANDS.inspectErlangBeam,
description:
"Inspect Erlang BEAM atoms, exports, imports and Code header without executing bytecode",
positionals: ["path"],
callerPaths: ["path"],
evidenceDelivery: true,
viewDelivery: { invalidJson: "view must be valid JSON" },
fields: {
path: { description: "Local BEAM file" },
view: {
description:
"JSON beam view; requires --detail summary and --evidence_output",
},
},
},
deliverWithService(createBeamService, "rea-erlang-beam"),
);
};
+30 -55
View File
@@ -1,19 +1,12 @@
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
import { z } from "incur";
import { evmInterfaceModule } from "../composition/evm.js";
import { logCliCommand } from "../cliLogging.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { localPathStringSchema } from "../domain/localPath.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { finishCliEvidenceCommand } from "./cliEvidenceCallerPath.js";
import { cliDecimalField } from "./options.js";
import { evmInterfaceModule } from "../composition/evm.js";
import {
inspectEvmInterfaceInputSchema,
evmProducerLimitsSchema,
} from "../domain/evm/evmInterface.js";
import { producerLimitsOption } from "./producerLimitsOption.js";
inspectWithService,
registerContractCommand,
} from "./contractCommand.js";
import { PRODUCER_LIMITS_HELP } from "./producerLimitsOption.js";
import type { CliInstance } from "./types.js";
/** Resolve operator paths and preserve explicit encoding through the shared workflow. */
export const registerEvmCommands = (
@@ -22,45 +15,27 @@ export const registerEvmCommands = (
environment: Readonly<NodeJS.ProcessEnv>,
): void => {
const service = evmInterfaceModule.create(environment);
cli.command(evmInterfaceModule.cliCommand, {
description: "Infer interface candidates from explicit local EVM bytecode",
args: z.object({
path: localPathStringSchema.describe("Local bytecode carrier"),
encoding: z
.enum(["raw", "hex"])
.describe("Selected carrier representation"),
}),
options: z.object({
producerLimits: producerLimitsOption(evmProducerLimitsSchema),
timeoutMs: cliDecimalField(
inspectEvmInterfaceInputSchema.shape.timeout_ms,
),
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, evmInterfaceModule.cliCommand, () =>
finishCliEvidenceCommand(
args.path,
() =>
service.inspect(
{
path: absoluteCallerPath(args.path),
encoding: args.encoding,
...(options.producerLimits === undefined
? {}
: { producer_limits: options.producerLimits }),
...(options.timeoutMs === undefined
? {}
: { timeout_ms: options.timeoutMs }),
},
{ signal },
),
service,
"evmole-interface",
"inspect_evm_interface",
signal,
).then(projectCliAnalysisResult),
),
),
});
registerContractCommand(
cli,
logger,
evmInterfaceModule.contract,
{
command: evmInterfaceModule.cliCommand,
description:
"Infer interface candidates from explicit local EVM bytecode",
positionals: ["path", "encoding"],
callerPaths: ["path"],
jsonText: ["producer_limits"],
fields: {
path: { description: "Local bytecode carrier" },
encoding: { description: "Selected carrier representation" },
producer_limits: { description: PRODUCER_LIMITS_HELP },
},
omit: {
detail:
"One-shot output is complete Evidence; this command has no summary delivery",
},
},
inspectWithService(() => service, "evmole-interface"),
);
};
+42 -92
View File
@@ -1,102 +1,52 @@
import { parseCliJsonOption } from "./cliJsonOption.js";
import { inspectModelArtifactInputSchema } from "../domain/model/modelArtifactQuery.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { z } from "incur";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { localPathStringSchema } from "../domain/localPath.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { createModelArtifactService } from "../composition/model.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { createModelArtifactService } from "../composition/model.js";
import { INSPECT_MODEL_ARTIFACT_CONTRACT } from "../contracts/model/modelToolContracts.js";
import {
analysisEvidenceDeliveryOptions,
deliverCliAnalysisEvidence,
} from "./analysisEvidenceDelivery.js";
deliverWithService,
registerContractCommand,
} from "./contractCommand.js";
import type { CliInstance } from "./types.js";
/** One-shot summaries export the same complete observations used by MCP retained pages. */
export const registerModelCommands = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.inspectModelArtifact, {
description:
"Inspect GGUF or safetensors headers without loading model weights",
args: z.object({
path: localPathStringSchema.describe(
"Local model file, detected by content",
),
}),
options: z.object({
...analysisEvidenceDeliveryOptions,
view: z
.string()
.optional()
.describe(
"JSON model page selection; requires --detail summary and --evidence_output",
),
hash: z
.enum(["header", "full"])
.default("header")
.describe(
"header reads declarations only; full additionally streams all bytes for SHA-256",
),
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectModelArtifact, async () => {
const decoded = parseCliJsonOption(
"inspect_model_artifact",
"view",
options.view,
"view must be a JSON model page selection",
);
if (!decoded.ok) return projectAnalysisError(decoded.error);
const view = decoded.value;
const selected = inspectModelArtifactInputSchema.safeParse({
path: absoluteCallerPath(args.path),
hash: options.hash,
detail: options.detail,
...(view === undefined ? {} : { view }),
});
if (!selected.success)
return projectAnalysisError(
analysisInputErrorFromIssues(
"inspect_model_artifact",
selected.error.issues,
{ view },
),
);
const service = createModelArtifactService();
return deliverCliAnalysisEvidence({
callerInputPath: args.path,
run: () =>
service.inspect(
{
path: selected.data.path,
hash: selected.data.hash,
detail: "complete",
},
{ signal },
),
service,
providerId: "model-artifact",
operation: "inspect_model_artifact",
signal,
detail: options.detail,
evidenceOutput: options.evidence_output,
...(selected.data.view === undefined
? {}
: {
view: {
kind: "model-artifact" as const,
...selected.data.view,
},
}),
});
}),
),
});
const deliver = deliverWithService(
createModelArtifactService,
"model-artifact",
);
registerContractCommand(
cli,
logger,
INSPECT_MODEL_ARTIFACT_CONTRACT,
{
command: CLI_COMMANDS.inspectModelArtifact,
description:
"Inspect GGUF or safetensors headers without loading model weights",
positionals: ["path"],
callerPaths: ["path"],
evidenceDelivery: true,
viewDelivery: {
invalidJson: "view must be a JSON model page selection",
kind: "model-artifact",
},
fields: {
path: { description: "Local model file, detected by content" },
hash: {
description:
"header reads declarations only; full additionally streams all bytes for SHA-256",
},
view: {
description:
"JSON model page selection; requires --detail summary and --evidence_output",
},
},
},
// The CLI delivers the summary itself, so the service returns complete
// Evidence; the contract's own default would retain a summary instead.
(input, context) => deliver({ ...input, detail: "complete" }, context),
);
};
+5 -3
View File
@@ -1,6 +1,10 @@
import { z } from "incur";
import { safeParseJson } from "../domain/safeJson.js";
/** CLI help for a producer's `producer_limits` JSON text. */
export const PRODUCER_LIMITS_HELP =
"JSON producer budget overrides. Keys and units follow this operation's producer_limits contract; changes apply on this call without restart and preserve tighter inherited OS limits.";
/** Incur passes object flags as text; validate the selected producer's exact policy. */
export const producerLimitsOption = <Schema extends z.ZodType>(
schema: Schema,
@@ -21,6 +25,4 @@ export const producerLimitsOption = <Schema extends z.ZodType>(
})
.pipe(schema)
.optional()
.describe(
"JSON producer budget overrides. Keys and units follow this operation's producer_limits contract; changes apply on this call without restart and preserve tighter inherited OS limits.",
);
.describe(PRODUCER_LIMITS_HELP);
+31 -79
View File
@@ -1,88 +1,40 @@
import { z } from "incur";
import { localPathStringSchema } from "../domain/localPath.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { createPythonPycService } from "../composition/pythonPyc.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { inspectPythonPycInputSchema } from "../domain/python/pythonPyc.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { createPythonPycService } from "../composition/pythonPyc.js";
import { PYTHON_PYC_TOOL_CONTRACTS } from "../contracts/python/pythonPycToolContracts.js";
import { contractNamed } from "./contractCliDescription.js";
import {
analysisEvidenceDeliveryOptions,
deliverCliAnalysisEvidence,
parseCliAnalysisEvidenceDelivery,
} from "./analysisEvidenceDelivery.js";
import { pythonPycDeliverySchema } from "../domain/analysisView/pythonPycView.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { cliDecimalField } from "./options.js";
deliverWithService,
registerContractCommand,
} from "./contractCommand.js";
import type { CliInstance } from "./types.js";
/** Inspect operator-selected pyc files through the same shared workflow as MCP. */
export const registerPythonPycCommands = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.inspectPythonPyc, {
description:
"Inspect a supported CPython 3.7–3.15 pyc header and marshal code objects without executing bytecode",
args: z.object({
path: localPathStringSchema.describe("Local CPython .pyc file"),
}),
options: z.object({
// Decimal tokens only: numeric coercion would read "0x10" as 16 and
// "1e2" as 100. The shared fields keep their bounds and defaults.
maxFileBytes: cliDecimalField(
inspectPythonPycInputSchema.shape.max_file_bytes,
),
maxObjects: cliDecimalField(
inspectPythonPycInputSchema.shape.max_objects,
),
maxDepth: cliDecimalField(inspectPythonPycInputSchema.shape.max_depth),
maxStringBytes: cliDecimalField(
inspectPythonPycInputSchema.shape.max_string_bytes,
),
...analysisEvidenceDeliveryOptions,
view: z
.string()
.optional()
.describe(
"JSON python-pyc view; requires --detail summary and --evidence_output",
),
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectPythonPyc, async () => {
const delivery = parseCliAnalysisEvidenceDelivery(
"inspect_python_pyc",
options,
pythonPycDeliverySchema,
);
if (!delivery.ok) return projectAnalysisError(delivery.error);
const service = createPythonPycService();
return deliverCliAnalysisEvidence({
callerInputPath: args.path,
run: () =>
service.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_objects: options.maxObjects,
max_depth: options.maxDepth,
max_string_bytes: options.maxStringBytes,
},
{ signal },
),
service,
providerId: "rea-python-pyc",
operation: "inspect_python_pyc",
signal,
detail: delivery.value.detail,
evidenceOutput: delivery.value.evidenceOutput,
...(delivery.value.view === undefined
? {}
: { view: delivery.value.view }),
});
}),
),
});
registerContractCommand(
cli,
logger,
contractNamed(PYTHON_PYC_TOOL_CONTRACTS, "inspect_python_pyc"),
{
command: CLI_COMMANDS.inspectPythonPyc,
description:
"Inspect a supported CPython 3.7–3.15 pyc header and marshal code objects without executing bytecode",
positionals: ["path"],
callerPaths: ["path"],
evidenceDelivery: true,
viewDelivery: { invalidJson: "view must be valid JSON" },
fields: {
path: { description: "Local CPython .pyc file" },
view: {
description:
"JSON python-pyc view; requires --detail summary and --evidence_output",
},
},
},
deliverWithService(createPythonPycService, "rea-python-pyc"),
);
};
+48 -119
View File
@@ -1,128 +1,57 @@
import { parseCliJsonOption } from "./cliJsonOption.js";
import { z } from "incur";
import { producerLimitsOption } from "./producerLimitsOption.js";
import { rustProducerLimitsSchema } from "../domain/rust/rustResourceLimits.js";
import { inspectRustBinaryInputSchema } from "../domain/rust/rustBinary.js";
import { cliDecimalField } from "./options.js";
import { createRustBinaryService } from "../composition/rust.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import {
analysisEvidenceDeliveryOptions,
deliverCliAnalysisEvidence,
} from "./analysisEvidenceDelivery.js";
import { inspectRustBinaryRequestSchema } from "../domain/rust/rustBinaryQuery.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { Logger } from "pino";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { createRustBinaryService } from "../composition/rust.js";
import { RUST_TOOL_CONTRACTS } from "../contracts/rust/rustToolContracts.js";
import { contractNamed } from "./contractCliDescription.js";
import {
deliverWithService,
registerContractCommand,
} from "./contractCommand.js";
import { PRODUCER_LIMITS_HELP } from "./producerLimitsOption.js";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { localPathStringSchema } from "../domain/localPath.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { machoSliceArchitectureSchema } from "../domain/apple/machoTargetSelection.js";
/** Resolve caller paths and inspect embedded Rust dependencies through the shared workflow. */
export const registerRustCommands = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.inspectRustBinary, {
description:
"Inspect an embedded cargo-auditable Rust dependency list without target execution",
args: z.object({
path: localPathStringSchema.describe(
"Local ELF, PE, thin Mach-O or universal Mach-O image",
),
}),
options: z.object({
...analysisEvidenceDeliveryOptions,
producerLimits: producerLimitsOption(rustProducerLimitsSchema),
maxRustcHashes: cliDecimalField(
inspectRustBinaryInputSchema.shape.max_rustc_hashes,
),
architecture: machoSliceArchitectureSchema
.optional()
.describe(
"Slice to read from a universal Mach-O, by CPU family; when several slices share a family, arm64 (arm64 or arm64v8), arm64e (arm64e or arm64e.x1) or x86_64 names the subtype, and a value matching several slices is refused; required for universal files",
),
detail: z
.enum(["complete", "summary"])
.optional()
.describe(
"Default complete; a view selects compact delivery with --evidence_output",
),
view: z
.string()
.optional()
.describe(
"JSON package or edge selection; retains complete Evidence with --evidence_output",
),
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectRustBinary, async () => {
const decoded = parseCliJsonOption(
"inspect_rust_binary",
"view",
options.view,
"view must be a JSON package or edge selection",
);
if (!decoded.ok) return projectAnalysisError(decoded.error);
const view = decoded.value;
const input = inspectRustBinaryRequestSchema.safeParse({
path: absoluteCallerPath(args.path),
...(options.producerLimits === undefined
? {}
: { producer_limits: options.producerLimits }),
...(options.maxRustcHashes === undefined
? {}
: { max_rustc_hashes: options.maxRustcHashes }),
...(options.architecture === undefined
? {}
: { architecture: options.architecture }),
...(options.detail === undefined ? {} : { detail: options.detail }),
...(view === undefined ? {} : { view }),
});
if (!input.success)
return projectAnalysisError(
analysisInputErrorFromIssues(
"inspect_rust_binary",
input.error.issues,
{ view },
),
);
const service = createRustBinaryService();
return deliverCliAnalysisEvidence({
callerInputPath: args.path,
run: () =>
service.inspect(
{
path: input.data.path,
...(input.data.producer_limits === undefined
? {}
: { producer_limits: input.data.producer_limits }),
...(input.data.max_rustc_hashes === undefined
? {}
: { max_rustc_hashes: input.data.max_rustc_hashes }),
...(input.data.architecture === undefined
? {}
: { architecture: input.data.architecture }),
},
{ signal },
),
service,
providerId: "rust-auditable",
operation: "inspect_rust_binary",
signal,
detail:
input.data.detail ??
(input.data.view === undefined ? "complete" : "summary"),
evidenceOutput: options.evidence_output,
...(input.data.view === undefined
? {}
: { view: { kind: "rust" as const, ...input.data.view } }),
});
}),
),
});
registerContractCommand(
cli,
logger,
contractNamed(RUST_TOOL_CONTRACTS, "inspect_rust_binary"),
{
command: CLI_COMMANDS.inspectRustBinary,
description:
"Inspect an embedded cargo-auditable Rust dependency list without target execution",
positionals: ["path"],
callerPaths: ["path"],
jsonText: ["producer_limits"],
evidenceDelivery: true,
viewDelivery: {
invalidJson: "view must be a JSON package or edge selection",
kind: "rust",
viewSelectsSummary: true,
},
fields: {
path: {
description: "Local ELF, PE, thin Mach-O or universal Mach-O image",
},
producer_limits: { description: PRODUCER_LIMITS_HELP },
architecture: {
description:
"Slice to read from a universal Mach-O, by CPU family; when several slices share a family, arm64 (arm64 or arm64v8), arm64e (arm64e or arm64e.x1) or x86_64 names the subtype, and a value matching several slices is refused; required for universal files",
},
detail: {
description:
"Default complete; a view selects compact delivery with --evidence_output",
},
view: {
description:
"JSON package or edge selection; retains complete Evidence with --evidence_output",
},
},
},
deliverWithService(createRustBinaryService, "rust-auditable"),
);
};
@@ -111,10 +111,6 @@ export const erlangBeamSummaryDeliveryShape = {
detail: z.literal("summary"),
view: erlangBeamViewRequestSchema.exactOptional(),
};
export const erlangBeamDeliverySchema = z.discriminatedUnion("detail", [
z.strictObject(erlangBeamCompleteDeliveryShape),
z.strictObject(erlangBeamSummaryDeliveryShape),
]);
const readReport = createAnalysisViewResultReader(erlangBeamSchema);
type Table = "atoms" | "exports" | "imports" | "lambdas" | "chunks";
type Row = NonNullable<ErlangBeam[Table]>[number];
-4
View File
@@ -40,10 +40,6 @@ export const pythonPycSummaryDeliveryShape = {
detail: z.literal("summary"),
view: pythonPycViewRequestSchema.exactOptional(),
};
export const pythonPycDeliverySchema = z.discriminatedUnion("detail", [
z.strictObject(pythonPycCompleteDeliveryShape),
z.strictObject(pythonPycSummaryDeliveryShape),
]);
export const pythonPycViewItemsSchema = z.array(
code.partial().extend({ source_index: z.int().nonnegative() }),
@@ -134,7 +134,38 @@ const registeredField = (
};
const contractShape = (description: ContractCliDescription) =>
description.contract.inputSchema.shape as Record<string, z.ZodType>;
description.fields;
/** A caller-relative target, or the first probe the positional admits. */
const positionalValue = (
description: ContractCliDescription,
parameter: ContractCliParameter,
): unknown =>
parameter.callerPath
? "rel/target"
: probes(contractShape(description)[parameter.field]!).find(
(value) =>
value !== undefined &&
admits(registeredField(description, parameter), value),
);
/** Parsed option values plus admitted positionals. */
const argvValues = (
description: ContractCliDescription,
options: Readonly<Record<string, unknown>>,
): Record<string, unknown> => ({
...(registered.get(description.command)!.options as z.ZodObject).parse(
options,
),
...Object.fromEntries(
description.parameters
.filter(({ positional }) => positional)
.map((parameter) => [
parameter.key,
positionalValue(description, parameter),
]),
),
});
const each = descriptions.map(
(description) => [description.command, description] as const,
@@ -150,6 +181,13 @@ describe("CLI commands projected from tool contracts", () => {
"inspect-devicetree",
"inspect-go-binary",
"inspect-v8-code-cache",
"inspect-rust-binary",
"inspect-python-pyc",
"inspect-erlang-beam",
"inspect-model-artifact",
"inspect-apk-signature",
"inspect-desktop-application",
"inspect-evm-interface",
]),
);
});
@@ -202,6 +240,12 @@ describe("CLI commands projected from tool contracts", () => {
expect(admits(contractField, "rel/x"), parameter.field).toBe(false);
expect(admits(contractField, absoluteCallerPath("rel/x"))).toBe(true);
}
if (parameter.viewSelection) {
// The view is JSON text decoded when the command runs; see below.
for (const value of [undefined, "", "{", "{}"])
expect(admits(cliField, value), parameter.key).toBe(true);
continue;
}
for (const value of probes(contractField)) {
const expected = expectedAdmission(parameter, contractField, value);
if (admits(cliField, value) !== expected)
@@ -233,15 +277,7 @@ describe("CLI commands projected from tool contracts", () => {
it.each(each)(
"%s maps parsed argv to an admitted contract input",
(_command, description) => {
const command = registered.get(description.command)!;
const values = {
...(command.options as z.ZodObject).parse({}),
...Object.fromEntries(
description.parameters
.filter(({ positional }) => positional)
.map(({ key }) => [key, "rel/target"]),
),
};
const values = argvValues(description, {});
const input = description.toContractInput(values);
expect(description.contract.inputSchema.safeParse(input).error).toBe(
undefined,
@@ -287,3 +323,98 @@ describe("CLI commands projected from tool contracts", () => {
).toThrow("probe input keyword not has no CLI projection");
});
});
const delivering = each.filter(
([, description]) => description.delivery !== undefined,
);
describe("Evidence delivery of contract commands", () => {
it.each(delivering)(
"%s refuses summary delivery without --evidence_output before analysis",
(_command, description) => {
const refused = description.delivery!(
argvValues(description, { detail: "summary" }),
);
expect(refused.ok).toBe(false);
if (!refused.ok)
expect(refused.error.issues.map(({ path }) => path)).toEqual([
["evidence_output"],
]);
expect(
description.delivery!(
argvValues(description, {
detail: "summary",
evidence_output: "out.json",
}),
).ok,
).toBe(true);
},
);
it.each(
delivering.filter(([, description]) =>
description.parameters.some(({ viewSelection }) => viewSelection),
),
)(
"%s decodes --view when it runs and admits what its contract admits",
(_command, description) => {
const view = description.parameters.find(
({ viewSelection }) => viewSelection,
)!;
const malformed = description.delivery!(
argvValues(description, {
detail: "summary",
evidence_output: "out.json",
[view.key]: "{",
}),
);
expect(malformed.ok).toBe(false);
if (!malformed.ok)
expect(malformed.error.issues).toEqual([
expect.objectContaining({ path: ["view"], reason: "invalid_format" }),
]);
const mismatches: string[] = [];
for (const detail of ["complete", "summary"] as const)
for (const text of ["{}", "[]", "null", '{"unknown":1}']) {
const values = argvValues(description, {
detail,
evidence_output: "out.json",
[view.key]: text,
});
const expected = description.contract.inputSchema.safeParse({
...description.toContractInput(values),
detail,
view: JSON.parse(text) as unknown,
}).success;
if (description.delivery!(values).ok !== expected)
mismatches.push(`${detail} ${text} expected ${String(expected)}`);
}
expect(mismatches).toEqual([]);
},
);
it("refuses a union input whose branches differ outside the delivery", () => {
const inputSchema = z.discriminatedUnion("detail", [
z.object({
detail: z.literal("complete").optional(),
limit: z.number().optional(),
}),
z.object({
detail: z.literal("summary"),
limit: z.number().max(1).optional(),
}),
]);
expect(() =>
describeContractCommand(
{ name: "probe", inputSchema },
{ command: "probe", description: "Probe", evidenceDelivery: true },
),
).toThrow("probe.limit differs between input branches");
expect(() =>
describeContractCommand(
{ name: "probe", inputSchema },
{ command: "probe", description: "Probe" },
),
).toThrow("probe.detail differs between input branches");
});
});