fix(cli): advertise parsed JSON documents in --schema

`--schema` for JSON-input CLI commands showed only an opaque
`inputJson` string, so callers could not see the document the command
actually parses. The argument now carries the decoded document as its
contentSchema, built from the same contract each command or its service
parses with (the Evidence input frame, or the ledger and coverage
request schemas). `$schema` is stripped, and a schema JSON Schema cannot
represent is refused rather than advertised as {}.

Fixes #9195
Supersedes #9283

Squashed from #9283 (with morluto's validation test) and rebased onto
main; the coverage test now expects boundary_id's stable-identifier
pattern from #9919. #9214 by 1052326311 took the same approach.

Co-Authored-By: morluto <76467478+morluto@users.noreply.github.com>
Co-Authored-By: 1052326311 <65798732+1052326311@users.noreply.github.com>
Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
authored and Ysopwnable committed 2026-11-14 14:16:39 +01:00
1 parent 3e95d22dd4
commit d734e6424d
15 files changed
+625 -66

No files matched your search

@@ -1,7 +1,6 @@
import { parseAnalysisInput } from "./AnalysisInput.js";
import { z } from "zod";
import { reconstructionCoverageEvaluationInputSchema } from "../domain/reconstructionCoverageInput.js";
import { reconstructionCoverageRequestSchema } from "../domain/reconstructionCoverageInput.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
evaluateParsedReconstructionClosure,
@@ -11,17 +10,17 @@ import {
import type { Result } from "../domain/result.js";
const OPERATION = "evaluate_reconstruction_coverage";
const requestEnvelopeSchema =
reconstructionCoverageEvaluationInputSchema.extend({
coverage: z.unknown(),
});
/** Evaluate inline reconstruction coverage against a named boundary. */
export const evaluateReconstructionCoverage = (
rawInput: unknown,
nowEpochMs = Date.now(),
): Result<ReconstructionClosureResult, AnalysisError> => {
const parsed = parseAnalysisInput(OPERATION, rawInput, requestEnvelopeSchema);
const parsed = parseAnalysisInput(
OPERATION,
rawInput,
reconstructionCoverageRequestSchema,
);
if (!parsed.ok) return parsed;
const coverage = readReconstructionCoverageData(parsed.value.coverage, [
"coverage",
@@ -16,9 +16,11 @@ import {
import { err, ok, type Result } from "../domain/result.js";
const OPERATION = "build_reconstruction_obligation_ledger";
const requestSchema = reconstructionObligationLedgerInputSchema.extend({
evidence_bundle: z.unknown(),
});
/** The ledger parser admits the bundle itself after this envelope. */
export const reconstructionObligationLedgerRequestSchema =
reconstructionObligationLedgerInputSchema.extend({
evidence_bundle: z.unknown(),
});
/** Authenticated bundle and its single request-owned record index. */
export interface ReconstructionObligationSources {
@@ -36,7 +38,11 @@ export interface ResolvedReconstructionObligationRequest {
export const resolveReconstructionObligationLedgerRequest = (
input: unknown,
): Result<ResolvedReconstructionObligationRequest, AnalysisError> => {
const frame = parseAnalysisInput(OPERATION, input, requestSchema);
const frame = parseAnalysisInput(
OPERATION,
input,
reconstructionObligationLedgerRequestSchema,
);
if (!frame.ok) return frame;
const bundle = readEvidenceBundle(frame.value.evidence_bundle, {
operation: OPERATION,
+18 -10
View File
@@ -18,6 +18,13 @@ import type { CliInstance } from "./types.js";
import { localPathStringSchema } from "../domain/localPath.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
import { jsonInputArgument } from "./jsonInputArgument.js";
import { inspectAnalysisViewInputSchema } from "../domain/analysisView/analysisViewRequest.js";
const inspectEvidenceBundleCliInputSchema =
getEvidenceBundleInputSchema.safeExtend({
path: importEvidenceBundleInputSchema.shape.path,
});
/** Project selected views of completed analysis Evidence through the CLI. */
export const registerAnalysisViewCommands = (
@@ -28,11 +35,10 @@ export const registerAnalysisViewCommands = (
description:
"Inspect a local Evidence bundle completely, as discovery metadata, as a page of selected complete records, or as direct-link and retained-closure membership.",
args: z.object({
inputJson: z
.string()
.describe(
"JSON or JSON file with absolute path, detail, optional record filters, records or links offset/limit, and links traversal/link_status",
),
inputJson: jsonInputArgument(
"JSON or JSON file with absolute path, detail, optional record filters, records or links offset/limit, and links traversal/link_status",
inspectEvidenceBundleCliInputSchema,
),
}),
run: ({ args }) =>
withCommandCancellation((signal) =>
@@ -43,10 +49,9 @@ export const registerAnalysisViewCommands = (
signal,
);
if (!input.ok) return input.error;
const schema = getEvidenceBundleInputSchema.safeExtend({
path: importEvidenceBundleInputSchema.shape.path,
});
const parsed = schema.safeParse(input.value);
const parsed = inspectEvidenceBundleCliInputSchema.safeParse(
input.value,
);
if (!parsed.success)
return {
error: "Evidence bundle inspection failed",
@@ -75,7 +80,10 @@ export const registerAnalysisViewCommands = (
description:
"Project a selected view of completed Evidence JSON for a supported analysis operation. For inventory rows, use --format csv or --format tsv with --filter-output normalized_result.items and --columns logical_path,artifact_id (if those fields are included in the view).",
args: z.object({
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
inputJson: jsonInputArgument(
"Inline workflow JSON or JSON file path",
inspectAnalysisViewInputSchema,
),
}),
options: z.object({
evidenceBundle: localPathStringSchema
+47 -2
View File
@@ -13,6 +13,17 @@ import type { Logger } from "pino";
import { registerSourceToBundleCommand } from "./sourceToBundleCommand.js";
import { registerStaticObservedEndpointsCommand } from "./staticObservedEndpointsCommand.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { getEvidenceInputContract } from "../domain/evidenceInputContract.js";
import { applicationInventoryProjectionInputSchema } from "../domain/mobileApplicationGraphProjection.js";
import {
compareApplicationVersionsRequestSchema,
compareJavaScriptExportShapesRequestSchema,
traceApplicationFeatureRequestSchema,
traceJavaScriptSemanticsRequestSchema,
} from "../contracts/javascript/applicationWorkflowInputContracts.js";
import { reconstructionCoverageRequestSchema } from "../domain/reconstructionCoverageInput.js";
import { reconstructionObligationLedgerRequestSchema } from "../application/ReconstructionObligationLedgerRequest.js";
import { jsonInputArgument } from "./jsonInputArgument.js";
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
import type { Evidence } from "../domain/evidence.js";
import type { Result } from "../domain/result.js";
@@ -24,6 +35,23 @@ import { withCommandCancellation } from "./commandCancellation.js";
type CliInstance = ReturnType<typeof Cli.create>;
const traceApplicationFeatureDocumentSchema = getEvidenceInputContract(
traceApplicationFeatureRequestSchema,
).frameSchema;
const traceJavaScriptSemanticsDocumentSchema = getEvidenceInputContract(
traceJavaScriptSemanticsRequestSchema,
).frameSchema;
const compareApplicationVersionsDocumentSchema = getEvidenceInputContract(
compareApplicationVersionsRequestSchema,
).frameSchema;
const compareJavaScriptExportShapesDocumentSchema = getEvidenceInputContract(
compareJavaScriptExportShapesRequestSchema,
).frameSchema;
const applicationInventoryDocumentSchema = getEvidenceInputContract(
applicationInventoryProjectionInputSchema,
).frameSchema;
const workflowJsonDescription = "Inline workflow JSON or JSON file path";
/** Register CLI equivalents of provider-neutral application graph workflows. */
export const registerApplicationCommands = (
cli: ReturnType<typeof Cli.create>,
@@ -34,6 +62,7 @@ export const registerApplicationCommands = (
logger,
name: CLI_COMMANDS.traceApplicationFeature,
operation: "trace_application_feature",
documentSchema: traceApplicationFeatureDocumentSchema,
description:
"Trace a typed seed through authenticated application Evidence JSON",
workflow: async (input, lookup, signal) => {
@@ -48,6 +77,7 @@ export const registerApplicationCommands = (
logger,
name: CLI_COMMANDS.traceJavaScriptSemantics,
operation: "trace_javascript_semantics",
documentSchema: traceJavaScriptSemanticsDocumentSchema,
description:
"Trace bounded static semantic relations through authenticated application Evidence",
workflow: async (input, lookup, signal) => {
@@ -64,6 +94,7 @@ export const registerApplicationCommands = (
logger,
name: CLI_COMMANDS.compareApplicationVersions,
operation: "compare_application_versions",
documentSchema: compareApplicationVersionsDocumentSchema,
description:
"Compare two authenticated JavaScript Application Graph versions",
workflow: async (input, lookup, signal) => {
@@ -85,6 +116,7 @@ export const registerApplicationCommands = (
logger,
name: CLI_COMMANDS.compareJavaScriptExportShapes,
operation: "compare_javascript_export_shapes",
documentSchema: compareJavaScriptExportShapesDocumentSchema,
description:
"Compare exact static JavaScript export return shapes without execution",
workflow: async (input, lookup, signal) => {
@@ -103,6 +135,7 @@ export const registerApplicationCommands = (
logger,
name: CLI_COMMANDS.projectAndroidApplicationGraph,
operation: "project_android_application_graph",
documentSchema: applicationInventoryDocumentSchema,
description:
"Project authenticated APK inventory Evidence into an Android application graph",
workflow: async (input, lookup, signal) => {
@@ -119,6 +152,7 @@ export const registerApplicationCommands = (
logger,
name: CLI_COMMANDS.projectAppleApplicationGraph,
operation: "project_apple_application_graph",
documentSchema: applicationInventoryDocumentSchema,
description:
"Project authenticated IPA or macOS app inventory Evidence into an Apple application graph",
workflow: async (input, lookup, signal) => {
@@ -133,6 +167,7 @@ export const registerApplicationCommands = (
logger,
name: CLI_COMMANDS.projectHarmonyApplicationGraph,
operation: "project_harmony_application_graph",
documentSchema: applicationInventoryDocumentSchema,
description:
"Project authenticated HarmonyOS HAP, HSP, or App Pack inventory Evidence into an application graph",
workflow: async (input, lookup, signal) => {
@@ -157,6 +192,7 @@ const registerObligationLedgerCommand = (
description:
"Generate a deterministic Evidence-backed reconstruction obligation ledger page",
inputSchema: z.unknown(),
documentSchema: reconstructionObligationLedgerRequestSchema,
workflow: async (input) => {
const { buildReconstructionObligationLedgerEvidence } =
await import("../application/ReconstructionObligationLedgerService.js");
@@ -171,6 +207,7 @@ const registerCoverageCommand = (cli: CliInstance, logger: Logger): void =>
name: CLI_COMMANDS.evaluateReconstructionCoverage,
description: "Evaluate inline fail-closed reconstruction coverage",
inputSchema: z.unknown(),
documentSchema: reconstructionCoverageRequestSchema,
workflow: async (input) => {
const { evaluateReconstructionCoverage } =
await import("../application/ReconstructionCoverageService.js");
@@ -184,6 +221,8 @@ interface JsonCommandOptions<Schema extends z.ZodType> {
readonly name: string;
readonly description: string;
readonly inputSchema: Schema;
/** Contract advertised by `--schema`. Runtime parsing stays `inputSchema`. */
readonly documentSchema?: z.ZodType;
readonly workflow: (
input: z.output<Schema>,
) => Promise<
@@ -198,12 +237,16 @@ const registerJsonCommand = <Schema extends z.ZodType>({
name,
description,
inputSchema,
documentSchema,
workflow,
}: JsonCommandOptions<Schema>): void => {
cli.command(name, {
description,
args: z.object({
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
inputJson: jsonInputArgument(
workflowJsonDescription,
documentSchema ?? inputSchema,
),
}),
run: ({ args }) =>
logCliCommand(logger, name, async () => {
@@ -239,6 +282,7 @@ const registerEvidenceJsonCommand = ({
name,
operation,
description,
documentSchema,
workflow,
}: {
readonly cli: CliInstance;
@@ -246,6 +290,7 @@ const registerEvidenceJsonCommand = ({
readonly name: string;
readonly operation: string;
readonly description: string;
readonly documentSchema: z.ZodType;
readonly workflow: (
input: unknown,
lookup: EvidenceLookup | undefined,
@@ -255,7 +300,7 @@ const registerEvidenceJsonCommand = ({
cli.command(name, {
description,
args: z.object({
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
inputJson: jsonInputArgument(workflowJsonDescription, documentSchema),
}),
options: z.object({ evidenceBundle: evidenceBundleOption }),
run: ({ args, options }) =>
+6 -1
View File
@@ -6,12 +6,14 @@ import {
resolveArtifactComparisonSources,
} from "../application/artifacts/ArtifactComparisonEvidence.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { artifactComparisonInputSchema } from "../contracts/artifactComparisonContracts.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { parseCliJsonInput, resolveCliJsonPaths } from "../cliJsonInput.js";
import { logCliCommand } from "../cliLogging.js";
import { ARTIFACT_COMPARISON_PROVIDER } from "../domain/comparisonEvidenceIdentity.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
import { jsonInputArgument } from "./jsonInputArgument.js";
import type { CliInstance } from "./types.js";
const OPERATION = "compare_artifacts";
@@ -26,7 +28,10 @@ export const registerArtifactComparisonCommand = (
description:
'Compare two artifact inventories, including FIT and legacy uImage firmware components, named as {left, right}: complete Evidence or {kind:"evidence-file",path,evidence_id?}. A one-shot command retains no session Evidence for retained-evidence references.',
args: z.object({
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
inputJson: jsonInputArgument(
"Inline workflow JSON or JSON file path",
artifactComparisonInputSchema,
),
}),
run: ({ args }) =>
withCommandCancellation((signal) =>
+12 -10
View File
@@ -16,6 +16,8 @@ import {
listElectronTargetsInputSchema,
} from "../domain/javascript/electronObservation.js";
import { electronActiveObservationInputSchema } from "../domain/javascript/electronActiveObservation.js";
import { reconcileJavaScriptRuntimeRequestSchema } from "../contracts/javascript/applicationWorkflowInputContracts.js";
import { getEvidenceInputContract } from "../domain/evidenceInputContract.js";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { projectInputIssues } from "../domain/inputIssueProjection.js";
@@ -42,6 +44,7 @@ import {
evidenceBundleOption,
runEvidenceInputCommand,
} from "./evidenceInputCommand.js";
import { jsonInputArgument } from "./jsonInputArgument.js";
/** Register CLI equivalents of the Electron MCP tools. */
export const registerElectronCommands = (
@@ -116,11 +119,10 @@ const registerElectronActiveCommand = (
cli.command(CLI_COMMANDS.captureElectronScenario, {
description: "Run one owned Electron scenario with cancellable actions",
args: z.object({
inputJson: z
.string()
.describe(
"Inline JSON or a JSON file matching capture_electron_scenario",
),
inputJson: jsonInputArgument(
"Inline JSON or a JSON file matching capture_electron_scenario",
electronActiveObservationInputSchema,
),
}),
run: ({ args }) =>
withCommandCancellation((signal) =>
@@ -184,11 +186,11 @@ const registerJavaScriptRuntimeReconciliationCommand = (
description:
"Reconcile static application and passive runtime Evidence JSON",
args: z.object({
inputJson: z
.string()
.describe(
"Inline JSON or a JSON file path matching reconcile_javascript_runtime",
),
inputJson: jsonInputArgument(
"Inline JSON or a JSON file path matching reconcile_javascript_runtime",
getEvidenceInputContract(reconcileJavaScriptRuntimeRequestSchema)
.frameSchema,
),
}),
options: z.object({ evidenceBundle: evidenceBundleOption }),
run: ({ args, options }) =>
+73
View File
@@ -0,0 +1,73 @@
import { z } from "incur";
const JSON_SCHEMA_DIALECT = "$schema";
const wrapperTypes = new Set([
"optional",
"nonoptional",
"nullable",
"default",
"prefault",
"readonly",
"catch",
]);
const innerSchema = (schema: z.ZodType): z.ZodType | undefined => {
if (!("unwrap" in schema) || typeof schema.unwrap !== "function")
return undefined;
const inner: unknown = schema.unwrap();
return inner instanceof z.ZodType ? inner : undefined;
};
/** Skip wrappers so `unknown` and `any` are recognized as the whole document. */
const documentType = (schema: z.ZodType): string => {
let current = schema;
for (;;) {
const type = current._zod.def.type;
if (!wrapperTypes.has(type)) return type;
const inner = innerSchema(current);
if (inner === undefined) return type;
current = inner;
}
};
/**
* JSON Schema for one positional JSON document.
*
* `{}` is only returned for a document the handler parses as `unknown` or
* `any`. Any other empty conversion is omitted: a custom or unrepresentable
* document must not be advertised as accepting every value. Input mode keeps
* the type a transform accepts, so a string transform stays a string.
*/
export const jsonInputContentSchema = (
schema: z.ZodType,
): Record<string, unknown> | undefined => {
let converted: Record<string, unknown>;
try {
converted = z.toJSONSchema(schema, {
io: "input",
unrepresentable: "any",
}) as Record<string, unknown>;
} catch {
return undefined;
}
const content = { ...converted };
delete content[JSON_SCHEMA_DIALECT];
if (Object.keys(content).length > 0) return content;
const type = documentType(schema);
return type === "unknown" || type === "any" ? {} : undefined;
};
/** Keep the positional string and attach the document contract Incur can see. */
export const jsonInputArgument = (
description: string,
schema: z.ZodType,
): z.ZodString => {
const argument = z.string().describe(description);
const contentSchema = jsonInputContentSchema(schema);
return contentSchema === undefined
? argument.meta({ contentMediaType: "application/json" })
: argument.meta({
contentMediaType: "application/json",
contentSchema,
});
};
+14 -6
View File
@@ -19,6 +19,10 @@ import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { registerProjectManagedApplicationGraph } from "./managedProjectGraphCommand.js";
import { managedMembersInputSchema } from "../contracts/managed/managedToolContracts.js";
import { managedReconstructionImportRequestSchema } from "../domain/managed/managedReconstruction.js";
import { managedNativeVerificationRequestSchema } from "../domain/managed/managedNativeVerificationSchemas.js";
import { getEvidenceInputContract } from "../domain/evidenceInputContract.js";
import { jsonInputArgument } from "./jsonInputArgument.js";
import { localPathStringSchema } from "../domain/localPath.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
@@ -240,9 +244,11 @@ const registerImportManagedReconstruction = (
description:
"Import decompiler-produced managed reconstruction against exact static member evidence",
args: z.object({
inputJson: z
.string()
.describe("Inline managed reconstruction JSON or JSON file path"),
inputJson: jsonInputArgument(
"Inline managed reconstruction JSON or JSON file path",
getEvidenceInputContract(managedReconstructionImportRequestSchema)
.frameSchema,
),
}),
options: z.object({ evidenceBundle: evidenceBundleOption }),
run: ({ args, options }) =>
@@ -278,9 +284,11 @@ const registerVerifyManagedNativeBoundaries = (
description:
"Verify managed P/Invoke declarations against authenticated native Evidence",
args: z.object({
inputJson: z
.string()
.describe("Inline managed/native verification JSON or JSON file path"),
inputJson: jsonInputArgument(
"Inline managed/native verification JSON or JSON file path",
getEvidenceInputContract(managedNativeVerificationRequestSchema)
.frameSchema,
),
}),
options: z.object({ evidenceBundle: evidenceBundleOption }),
run: ({ args, options }) =>
+8 -3
View File
@@ -11,6 +11,9 @@ import {
runEvidenceInputCommand,
} from "./evidenceInputCommand.js";
import type { Logger } from "pino";
import { managedApplicationGraphReferenceInputSchema } from "../domain/managed/managedApplicationGraph.js";
import { getEvidenceInputContract } from "../domain/evidenceInputContract.js";
import { jsonInputArgument } from "./jsonInputArgument.js";
import type { CliInstance } from "./types.js";
/** Register managed static-Evidence application graph projection. */
@@ -22,9 +25,11 @@ export const registerProjectManagedApplicationGraph = (
description:
"Project authenticated managed static Evidence into an application graph",
args: z.object({
inputJson: z
.string()
.describe("Inline managed application graph JSON or JSON file path"),
inputJson: jsonInputArgument(
"Inline managed application graph JSON or JSON file path",
getEvidenceInputContract(managedApplicationGraphReferenceInputSchema)
.frameSchema,
),
}),
options: z.object({ evidenceBundle: evidenceBundleOption }),
run: ({ args, options }) =>
+5 -5
View File
@@ -12,6 +12,7 @@ import { localPathStringSchema } from "../domain/localPath.js";
import { nativeCallObservationInputSchema } from "../domain/native/nativeCallObservation.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { jsonInputArgument } from "./jsonInputArgument.js";
const OPERATION = "observe_native_calls";
@@ -26,11 +27,10 @@ export const registerNativeCallCommands = (
"Launch a Mach-O under LLDB and record entries into selected functions or Objective-C methods",
args: z.object({
path: localPathStringSchema.describe("Mach-O or app path"),
inputJson: z
.string()
.describe(
"Inline JSON or a JSON file path: breakpoints, arguments, environment, duration_ms, max_events, argument_registers, backtrace_frames, capture_limits",
),
inputJson: jsonInputArgument(
"Inline JSON or a JSON file path: breakpoints, arguments, environment, duration_ms, max_events, argument_registers, backtrace_frames, capture_limits",
nativeCallObservationInputSchema,
),
}),
run: ({ args }) =>
logCliCommand(logger, CLI_COMMANDS.observeNativeCalls, async () => {
+7 -1
View File
@@ -5,7 +5,9 @@ import { CLI_COMMANDS } from "../cliCommandNames.js";
import { parseCliJsonInput } from "../cliJsonInput.js";
import { logCliCommand } from "../cliLogging.js";
import { compareSourceToBundleCliInputSchema } from "../contracts/javascript/applicationWorkflowInputContracts.js";
import { getEvidenceInputContract } from "../domain/evidenceInputContract.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { jsonInputArgument } from "./jsonInputArgument.js";
import {
evidenceBundleOption,
runEvidenceInputCommand,
@@ -21,7 +23,11 @@ export const registerSourceToBundleCommand = (
description:
"Compare historical source (inline graph or reference {kind:file,path}) with authenticated application Evidence. Save the complete JSON to query with inspect-analysis-view.",
args: z.object({
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
inputJson: jsonInputArgument(
"Inline workflow JSON or JSON file path",
getEvidenceInputContract(compareSourceToBundleCliInputSchema)
.frameSchema,
),
}),
options: z.object({ evidenceBundle: evidenceBundleOption }),
run: ({ args, options }) =>
+9 -7
View File
@@ -6,6 +6,7 @@ import { CLI_COMMANDS } from "../cliCommandNames.js";
import { parseCliJsonInput } from "../cliJsonInput.js";
import { logCliCommand } from "../cliLogging.js";
import { compareStaticObservedEndpointsFactsSchema } from "../contracts/javascript/applicationWorkflowInputContracts.js";
import { jsonInputArgument } from "./jsonInputArgument.js";
import { getEvidenceInputContract } from "../domain/evidenceInputContract.js";
import { endpointComparisonViewRequestSchema } from "../domain/analysisView/endpointComparisonView.js";
import { parseAnalysisInput } from "../application/AnalysisInput.js";
@@ -23,6 +24,9 @@ import {
} from "./analysisEvidenceDelivery.js";
const operation = "compare_static_and_observed_endpoints";
const comparisonDocumentSchema = getEvidenceInputContract(
compareStaticObservedEndpointsFactsSchema,
).frameSchema;
const deliverySchema = z
.object({
detail: analysisEvidenceDeliveryOptions.detail,
@@ -73,8 +77,7 @@ const runComparison = async (
const input = parseAnalysisInput(
operation,
raw.value,
getEvidenceInputContract(compareStaticObservedEndpointsFactsSchema)
.frameSchema,
comparisonDocumentSchema,
signal,
);
if (!input.ok) return projectAnalysisError(input.error);
@@ -112,11 +115,10 @@ export const registerStaticObservedEndpointsCommand = (
description:
"Compare static JavaScript endpoints and HAR Evidence JSON offline; --detail summary with --evidence_output saves full facts for selected inspect-analysis-view pages",
args: z.object({
inputJson: z
.string()
.describe(
"Inline JSON or file containing application, capture and optional origin",
),
inputJson: jsonInputArgument(
"Inline JSON or file containing application, capture and optional origin",
comparisonDocumentSchema,
),
}),
options: commandOptions,
run: ({ args, options }) =>
+11 -10
View File
@@ -21,9 +21,12 @@ import {
import { err, ok, type Result } from "./domain/result.js";
import type { BrowserScenarioCapturePort } from "./application/BrowserScenarioCapturePort.js";
import type { JsonValue } from "./domain/jsonValue.js";
import { jsonInputArgument } from "./cli/jsonInputArgument.js";
import type { Logger } from "pino";
const OPERATION = "capture_browser_scenario";
const browserScenarioJsonDescription =
"Inline browser scenario JSON, JSON file path, or '-' for stdin";
/** Register controlled browser execution and offline validation commands. */
export const registerBrowserScenarioCommands = (
@@ -40,11 +43,10 @@ export const registerBrowserScenarioCommands = (
description:
"Run one bounded Playwright browser scenario and return Evidence",
args: z.object({
inputJson: z
.string()
.describe(
"Inline browser scenario JSON, JSON file path, or '-' for stdin",
),
inputJson: jsonInputArgument(
browserScenarioJsonDescription,
browserScenarioSchema,
),
}),
options: z.object(analysisEvidenceDeliveryOptions),
run: ({ args, options: delivery }) =>
@@ -110,11 +112,10 @@ const registerBrowserScenarioValidationCommand = (
description:
"Validate one browser scenario without launching or attaching to a browser",
args: z.object({
inputJson: z
.string()
.describe(
"Inline browser scenario JSON, JSON file path, or '-' for stdin",
),
inputJson: jsonInputArgument(
browserScenarioJsonDescription,
browserScenarioSchema,
),
}),
run: ({ args }) =>
withCommandCancellation((signal) =>
@@ -9,3 +9,12 @@ export const reconstructionCoverageEvaluationInputSchema = z.strictObject({
coverage: reconstructionCoverageDataSchema,
boundary_id: reconstructionBoundaryIdSchema,
});
/**
* Envelope the coverage command parses before its reader authenticates
* `coverage`. The reader owns that document, so this field stays unknown.
*/
export const reconstructionCoverageRequestSchema =
reconstructionCoverageEvaluationInputSchema.extend({
coverage: z.unknown(),
});
+390
View File
@@ -0,0 +1,390 @@
import { beforeAll, expect, it } from "vitest";
import { Ajv2020 } from "ajv/dist/2020.js";
import type { AnySchema } from "ajv";
import { z } from "zod";
import { createCli } from "../../../src/cli.js";
import { CLI_COMMANDS } from "../../../src/cliCommandNames.js";
import { artifactComparisonInputSchema } from "../../../src/contracts/artifactComparisonContracts.js";
import {
compareApplicationVersionsRequestSchema,
compareJavaScriptExportShapesRequestSchema,
compareSourceToBundleCliInputSchema,
compareStaticObservedEndpointsFactsSchema,
reconcileJavaScriptRuntimeRequestSchema,
traceApplicationFeatureRequestSchema,
traceJavaScriptSemanticsRequestSchema,
} from "../../../src/contracts/javascript/applicationWorkflowInputContracts.js";
import {
getEvidenceBundleInputSchema,
importEvidenceBundleInputSchema,
} from "../../../src/contracts/sessionToolSchemas.js";
import { reconstructionCoverageRequestSchema } from "../../../src/domain/reconstructionCoverageInput.js";
import { reconstructionObligationLedgerRequestSchema } from "../../../src/application/ReconstructionObligationLedgerRequest.js";
import { inspectAnalysisViewInputSchema } from "../../../src/domain/analysisView/analysisViewRequest.js";
import { browserScenarioSchema } from "../../../src/domain/browserScenario.js";
import { getEvidenceInputContract } from "../../../src/domain/evidenceInputContract.js";
import { electronActiveObservationInputSchema } from "../../../src/domain/javascript/electronActiveObservation.js";
import { applicationInventoryProjectionInputSchema } from "../../../src/domain/mobileApplicationGraphProjection.js";
import { managedApplicationGraphReferenceInputSchema } from "../../../src/domain/managed/managedApplicationGraph.js";
import { managedNativeVerificationRequestSchema } from "../../../src/domain/managed/managedNativeVerificationSchemas.js";
import { managedReconstructionImportRequestSchema } from "../../../src/domain/managed/managedReconstruction.js";
import { nativeCallObservationInputSchema } from "../../../src/domain/native/nativeCallObservation.js";
import {
jsonInputArgument,
jsonInputContentSchema,
} from "../../../src/cli/jsonInputArgument.js";
import { visitRegisteredCommands } from "../../../src/cli/visitRegisteredCommands.js";
import type { CliInstance } from "../../../src/cli/types.js";
const frame = (schema: z.ZodType): z.ZodType =>
getEvidenceInputContract(schema).frameSchema;
const inspectEvidenceBundleCliInputSchema =
getEvidenceBundleInputSchema.safeExtend({
path: importEvidenceBundleInputSchema.shape.path,
});
const inventoryDocumentSchema = frame(
applicationInventoryProjectionInputSchema,
);
/** Every positional JSON document and the schema its handler parses. */
const documents = [
{
command: CLI_COMMANDS.traceApplicationFeature,
schema: frame(traceApplicationFeatureRequestSchema),
},
{
command: CLI_COMMANDS.traceJavaScriptSemantics,
schema: frame(traceJavaScriptSemanticsRequestSchema),
},
{
command: CLI_COMMANDS.compareApplicationVersions,
schema: frame(compareApplicationVersionsRequestSchema),
},
{
command: CLI_COMMANDS.compareStaticAndObservedEndpoints,
schema: frame(compareStaticObservedEndpointsFactsSchema),
},
{
command: CLI_COMMANDS.compareSourceToBundle,
schema: frame(compareSourceToBundleCliInputSchema),
},
{
command: CLI_COMMANDS.compareJavaScriptExportShapes,
schema: frame(compareJavaScriptExportShapesRequestSchema),
},
{
command: CLI_COMMANDS.buildReconstructionObligationLedger,
schema: reconstructionObligationLedgerRequestSchema,
},
{
command: CLI_COMMANDS.evaluateReconstructionCoverage,
schema: reconstructionCoverageRequestSchema,
},
{
command: CLI_COMMANDS.projectAndroidApplicationGraph,
schema: inventoryDocumentSchema,
},
{
command: CLI_COMMANDS.projectAppleApplicationGraph,
schema: inventoryDocumentSchema,
},
{
command: CLI_COMMANDS.projectHarmonyApplicationGraph,
schema: inventoryDocumentSchema,
},
{
command: CLI_COMMANDS.inspectEvidenceBundle,
schema: inspectEvidenceBundleCliInputSchema,
},
{
command: CLI_COMMANDS.inspectAnalysisView,
schema: inspectAnalysisViewInputSchema,
},
{
command: CLI_COMMANDS.captureElectronScenario,
schema: electronActiveObservationInputSchema,
},
{
command: CLI_COMMANDS.reconcileJavaScriptRuntime,
schema: frame(reconcileJavaScriptRuntimeRequestSchema),
},
{
command: CLI_COMMANDS.observeNativeCalls,
schema: nativeCallObservationInputSchema,
},
{
command: CLI_COMMANDS.importManagedReconstruction,
schema: frame(managedReconstructionImportRequestSchema),
},
{
command: CLI_COMMANDS.verifyManagedNativeBoundaries,
schema: frame(managedNativeVerificationRequestSchema),
},
{
command: CLI_COMMANDS.projectManagedApplicationGraph,
schema: frame(managedApplicationGraphReferenceInputSchema),
},
{
command: CLI_COMMANDS.compareArtifacts,
schema: artifactComparisonInputSchema,
},
{
command: CLI_COMMANDS.captureBrowserScenario,
schema: browserScenarioSchema,
},
{
command: CLI_COMMANDS.validateBrowserScenario,
schema: browserScenarioSchema,
},
] as const;
interface InputJsonSchema {
readonly type?: unknown;
readonly description?: unknown;
readonly contentMediaType?: unknown;
readonly contentSchema?: {
readonly properties?: Readonly<Record<string, unknown>>;
readonly required?: readonly string[];
};
}
const jsonInputCommands = (cli: CliInstance): string[] => {
const names: string[] = [];
visitRegisteredCommands(cli, (command, path) => {
if (
command.args instanceof z.ZodObject &&
Object.hasOwn(command.args.shape, "inputJson")
)
names.push(path);
});
return names;
};
let cli: ReturnType<typeof createCli>;
beforeAll(() => {
cli = createCli({});
});
const serve = async (argv: readonly string[]) => {
let stdout = "";
let exitCode = 0;
await cli.serve([...argv], {
env: {},
exit: (code) => {
exitCode = code;
},
stdout: (text) => {
stdout += text;
},
});
return { stdout, exitCode };
};
const advertisedInput = (stdout: string): InputJsonSchema => {
const document = JSON.parse(stdout) as {
args?: {
properties?: { inputJson?: InputJsonSchema };
required?: readonly string[];
};
};
expect(document.args?.required).toContain("inputJson");
const inputJson = document.args?.properties?.inputJson;
expect(inputJson).toMatchObject({
type: "string",
contentMediaType: "application/json",
});
expect(typeof inputJson?.description).toBe("string");
return inputJson as InputJsonSchema;
};
it("advertises the parsed document for every JSON-input command", async () => {
const ajv = new Ajv2020({ strict: false, validateFormats: false });
expect(jsonInputCommands(cli).sort()).toEqual(
documents.map(({ command }) => command).sort(),
);
for (const { command, schema } of documents) {
const served = await serve([command, "--schema", "--json"]);
expect(served.exitCode, served.stdout).toBe(0);
const inputJson = advertisedInput(served.stdout);
expect(inputJson.contentSchema, command).toBeDefined();
expect(inputJson.contentSchema, command).toEqual(
jsonInputContentSchema(schema),
);
expect(inputJson.contentSchema, command).not.toEqual({});
expect(
() => ajv.compile(inputJson.contentSchema as AnySchema),
command,
).not.toThrow();
}
});
it("validates representative parsed documents with the advertised schema", async () => {
const input = advertisedInput(
(
await serve([
CLI_COMMANDS.evaluateReconstructionCoverage,
"--schema",
"--json",
])
).stdout,
);
const validate = new Ajv2020({
strict: false,
validateFormats: false,
}).compile(input.contentSchema as AnySchema);
const accepted = { coverage: { arbitrary: true }, boundary_id: "main" };
const rejected = [
{ coverage: {}, boundary_id: "" },
{ coverage: {} },
{ coverage: {}, boundary_id: "main", extra: true },
];
expect(reconstructionCoverageRequestSchema.safeParse(accepted).success).toBe(
true,
);
expect(validate(accepted)).toBe(true);
for (const document of rejected) {
expect(
reconstructionCoverageRequestSchema.safeParse(document).success,
).toBe(false);
expect(validate(document)).toBe(false);
}
});
it("names the feature-trace, semantic-query, and browser-scenario fields", async () => {
const trace = advertisedInput(
(await serve([CLI_COMMANDS.traceApplicationFeature, "--schema", "--json"]))
.stdout,
);
expect(trace.description).toBe("Inline workflow JSON or JSON file path");
expect(Object.keys(trace.contentSchema?.properties ?? {})).toEqual([
"native_observations",
"seed",
"semantic_page",
"direction",
"application",
]);
expect(trace.contentSchema?.required).toEqual(["seed", "application"]);
const semantic = advertisedInput(
(await serve([CLI_COMMANDS.traceJavaScriptSemantics, "--schema", "--json"]))
.stdout,
);
expect(Object.keys(semantic.contentSchema?.properties ?? {})).toEqual([
"application",
"query",
]);
expect(semantic.contentSchema?.required).toEqual(["application", "query"]);
const browser = advertisedInput(
(await serve([CLI_COMMANDS.captureBrowserScenario, "--schema", "--json"]))
.stdout,
);
expect(browser.description).toBe(
"Inline browser scenario JSON, JSON file path, or '-' for stdin",
);
expect(browser.contentSchema?.properties).toEqual(
expect.objectContaining({
browser: expect.any(Object),
start_url: expect.any(Object),
actions: expect.any(Object),
}),
);
expect(browser.contentSchema?.required).toEqual(
expect.arrayContaining(["browser", "start_url", "actions"]),
);
});
it("keeps unknown envelope fields permissive and named fields constrained", async () => {
const coverage = advertisedInput(
(
await serve([
CLI_COMMANDS.evaluateReconstructionCoverage,
"--schema",
"--json",
])
).stdout,
);
expect(coverage.contentSchema?.properties?.coverage).toEqual({});
// boundary_id is a stable identifier (#9919), so its pattern is advertised.
expect(coverage.contentSchema?.properties?.boundary_id).toMatchObject({
type: "string",
pattern: expect.any(String),
});
expect(coverage.contentSchema?.required).toEqual(["coverage", "boundary_id"]);
const ledger = advertisedInput(
(
await serve([
CLI_COMMANDS.buildReconstructionObligationLedger,
"--schema",
"--json",
])
).stdout,
);
expect(ledger.contentSchema?.properties?.evidence_bundle).toEqual({});
expect(ledger.contentSchema?.required).toEqual(
expect.arrayContaining([
"evidence_bundle",
"reviewed_obligations",
"manifest",
]),
);
});
it("prints the nested document in the default schema format", async () => {
const served = await serve([
CLI_COMMANDS.traceApplicationFeature,
"--schema",
]);
expect(served.exitCode, served.stdout).toBe(0);
expect(served.stdout).toContain("contentMediaType: application/json");
expect(served.stdout).toContain("contentSchema:");
expect(served.stdout).toContain("seed:");
expect(served.stdout).toContain("application:");
});
it("uses an empty schema only for unknown or any documents", () => {
expect(jsonInputContentSchema(z.unknown())).toEqual({});
expect(jsonInputContentSchema(z.any())).toEqual({});
expect(jsonInputContentSchema(z.unknown().nonoptional())).toEqual({});
const custom = z.custom<string>();
expect(jsonInputContentSchema(custom)).toBeUndefined();
const omitted = z.toJSONSchema(jsonInputArgument("payload", custom));
expect(omitted).toMatchObject({
type: "string",
description: "payload",
contentMediaType: "application/json",
});
expect(omitted).not.toHaveProperty("contentSchema");
const exploding = z.object({ value: z.string() });
exploding._zod.toJSONSchema = () => {
throw new Error("unconvertible");
};
expect(jsonInputContentSchema(exploding)).toBeUndefined();
expect(() =>
z.toJSONSchema(jsonInputArgument("payload", exploding)),
).not.toThrow();
const query = z.toJSONSchema(
jsonInputArgument(
"Inline workflow JSON or JSON file path",
z.strictObject({ query: z.string() }),
),
);
expect(query).toMatchObject({
type: "string",
description: "Inline workflow JSON or JSON file path",
contentMediaType: "application/json",
contentSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
additionalProperties: false,
},
});
});