From da9a580a0d7bb6b68a3924bf3765a536867e2395 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:07:43 +0800 Subject: [PATCH] refactor(javascript): unify application workflow ownership --- .../javascript/application-workflows.mjs | 24 +- scripts/verify/managed/conformance.mjs | 5 +- scripts/verify/managed/manifest.mjs | 4 +- .../JavaScriptApplicationEvidenceGraph.ts | 9 +- .../JavaScriptApplicationWorkflowService.ts | 407 ++++++++++-------- src/cli/applicationCommands.ts | 24 +- src/cli/sourceToBundleCommand.ts | 7 +- .../compareExportShapes.ts | 24 +- .../compareSourceToBundle.ts | 14 +- .../compareVersions.ts | 28 +- .../registerApplicationTools/traceFeature.ts | 19 +- .../cli/workflowInputIdentities.test.ts | 36 ++ .../javascriptApplicationWorkflows.test.ts | 8 +- .../javascriptExportContainerCoverage.test.ts | 12 +- .../mcp/applicationExportShapeMcp.test.ts | 5 +- .../managedApplicationGraph.test.ts | 2 +- .../derivedOperationFailures.test.ts | 54 +++ .../sourceToBundleReferenceGuards.test.ts | 18 +- 18 files changed, 385 insertions(+), 315 deletions(-) diff --git a/scripts/verify/javascript/application-workflows.mjs b/scripts/verify/javascript/application-workflows.mjs index cfd0c8f119..5a88b7e11e 100644 --- a/scripts/verify/javascript/application-workflows.mjs +++ b/scripts/verify/javascript/application-workflows.mjs @@ -9,9 +9,6 @@ import { compareJavaScriptExportShapesEvidence, traceApplicationFeatureEvidence, } from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js"; -import { applicationVersionComparisonResultSchema } from "../../../dist/domain/javascript/javascriptApplicationVersionComparisonSchemas.js"; -import { applicationFeatureTraceResultSchema } from "../../../dist/domain/javascript/javascriptFeatureTraceSchemas.js"; -import { javaScriptExportShapeComparisonResultSchema } from "../../../dist/domain/javascript/javascriptExportShapeComparisonSchemas.js"; import { completeVerifierRun, createVerifierRun, @@ -50,9 +47,7 @@ const [left, right] = await Promise.all([ ]); const comparison = compareApplicationVersionsEvidence({ left, right }); if (!comparison.ok) throw comparison.error; -const comparisonResult = applicationVersionComparisonResultSchema.parse( - comparison.value.normalized_result, -); +const comparisonResult = comparison.value.result; const seedValue = options["seed-value"] ?? left.normalized_result.graph.nodes.find( @@ -72,10 +67,7 @@ const trace = direction: "both", }); if (trace !== null && !trace.ok) throw trace.error; -const traceResult = - trace === null - ? null - : applicationFeatureTraceResultSchema.parse(trace.value.normalized_result); +const traceResult = trace === null ? null : trace.value.result; const exportShapeSelector = resolveExportShapeSelector(left, right, options); const exportShapeComparison = exportShapeSelector === null @@ -88,17 +80,13 @@ const exportShapeComparison = if (exportShapeComparison !== null && !exportShapeComparison.ok) throw exportShapeComparison.error; const exportShapeResult = - exportShapeComparison === null - ? null - : javaScriptExportShapeComparisonResultSchema.parse( - exportShapeComparison.value.normalized_result, - ); + exportShapeComparison === null ? null : exportShapeComparison.value.result; process.stdout.write( `${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), comparison_id: comparisonResult.comparison_id, - evidence_id: comparison.value.evidence_id, + evidence_id: comparison.value.evidence.evidence_id, left: comparisonResult.left, right: comparisonResult.right, summary: comparisonResult.summary, @@ -109,7 +97,7 @@ process.stdout.write( ? null : { comparison_id: exportShapeResult.comparison_id, - evidence_id: exportShapeComparison.value.evidence_id, + evidence_id: exportShapeComparison.value.evidence.evidence_id, left: exportShapeResult.left, right: exportShapeResult.right, summary: exportShapeResult.summary, @@ -122,7 +110,7 @@ process.stdout.write( ? null : { trace_id: traceResult.trace_id, - evidence_id: trace.value.evidence_id, + evidence_id: trace.value.evidence.evidence_id, seed: traceResult.seed, summary: traceResult.summary, coverage: traceResult.coverage, diff --git a/scripts/verify/managed/conformance.mjs b/scripts/verify/managed/conformance.mjs index 3dbf56742c..d326edc99c 100644 --- a/scripts/verify/managed/conformance.mjs +++ b/scripts/verify/managed/conformance.mjs @@ -202,10 +202,7 @@ try { direction: "incoming", }); assert.equal(applicationTrace.ok, true); - assert.equal( - applicationTrace.value.normalized_result.summary.matched_seeds, - 1, - ); + assert.equal(applicationTrace.value.result.summary.matched_seeds, 1); const readyToRun = await fixture("r2r-x64.exe", { machine: 0x8664, diff --git a/scripts/verify/managed/manifest.mjs b/scripts/verify/managed/manifest.mjs index ecf3f7e34f..e47c0c6cd3 100644 --- a/scripts/verify/managed/manifest.mjs +++ b/scripts/verify/managed/manifest.mjs @@ -207,7 +207,7 @@ function verifyManagedAppManifestApplicationGraph({ traced.ok, `application graph trace failed for method ${expectedTrace.method_token}`, ); - const matched = traced.value.normalized_result.summary.matched_seeds; + const matched = traced.value.result.summary.matched_seeds; ensure( matched >= expectedTrace.min_matched_seeds, `application graph trace ${expectedTrace.seed} matched ${String(matched)} seeds, expected at least ${String(expectedTrace.min_matched_seeds)}`, @@ -218,7 +218,7 @@ function verifyManagedAppManifestApplicationGraph({ method_token: expectedTrace.method_token, seed: expectedTrace.seed, matched_seeds: matched, - trace_evidence_id: traced.value.evidence_id, + trace_evidence_id: traced.value.evidence.evidence_id, }); } diff --git a/src/application/javascript/JavaScriptApplicationEvidenceGraph.ts b/src/application/javascript/JavaScriptApplicationEvidenceGraph.ts index 6c8e2e51c7..0e7b519ec4 100644 --- a/src/application/javascript/JavaScriptApplicationEvidenceGraph.ts +++ b/src/application/javascript/JavaScriptApplicationEvidenceGraph.ts @@ -320,7 +320,7 @@ const requireProvider = ( ); }; -/** Parse unique, artifact-bound Evidence that may extend a native handoff. */ +/** Authenticate artifact-bound Evidence that may extend a native handoff. */ export const parseNativeApplicationEvidence = ( inputs: readonly unknown[], path: readonly (string | number)[] = [], @@ -336,13 +336,6 @@ export const parseNativeApplicationEvidence = ( "subject", "Native handoff Evidence requires an artifact subject.", ); - const ids = parsed.map(({ evidence_id: id }) => id); - if (new Set(ids).size !== ids.length) - throw invalidEvidence( - path, - "evidence_id", - "Native handoff Evidence must be unique.", - ); return parsed; }); diff --git a/src/application/javascript/JavaScriptApplicationWorkflowService.ts b/src/application/javascript/JavaScriptApplicationWorkflowService.ts index c04703d611..41e0576d1a 100644 --- a/src/application/javascript/JavaScriptApplicationWorkflowService.ts +++ b/src/application/javascript/JavaScriptApplicationWorkflowService.ts @@ -1,7 +1,11 @@ +import { + resolveEvidenceInput, + type EvidenceLookup, +} from "../EvidenceInputResolver.js"; +import { retainedEvidenceReferenceSchema } from "../../domain/evidenceReference.js"; import type { ResolvedSourceToBundleReference } from "./SourceToBundleReference.js"; import { z } from "zod"; -import { AnalysisProtocolError } from "../../domain/analysisErrorCore.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { Evidence } from "../../domain/evidence.js"; import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; @@ -9,20 +13,26 @@ import { compareJavaScriptApplicationVersions } from "../../domain/javascript/ja import { compareApplicationVersionsInputSchema, applicationVersionIdentitiesSchema, + type ApplicationVersionComparisonResult, } from "../../domain/javascript/javascriptApplicationVersionComparisonSchemas.js"; import { compareJavaScriptExportShapes } from "../../domain/javascript/javascriptExportShapeComparison.js"; import { compareJavaScriptExportShapesInputSchema, javaScriptExportShapeIdentitiesSchema, + type JavaScriptExportShapeComparisonResult, } from "../../domain/javascript/javascriptExportShapeComparisonSchemas.js"; import { traceApplicationFeature } from "../../domain/javascript/javascriptFeatureTrace.js"; import { traceApplicationFeatureInputSchema, applicationTraceIdentitiesSchema, + type ApplicationFeatureTraceResult, } from "../../domain/javascript/javascriptFeatureTraceSchemas.js"; import { err, ok, type Result } from "../../domain/result.js"; import { compareSourceToBundle } from "../../domain/javascript/sourceToBundleComparison.js"; -import { compareSourceToBundleInputSchema } from "../../domain/javascript/sourceToBundleComparisonSchemas.js"; +import { + compareSourceToBundleInputSchema, + type SourceToBundleComparisonResult, +} from "../../domain/javascript/sourceToBundleComparisonSchemas.js"; import { applicationGraphEvidenceInputError, parseApplicationGraphEvidence, @@ -35,29 +45,61 @@ import { createSourceToBundleComparisonEvidence, } from "./JavaScriptApplicationWorkflowEvidence.js"; +/** Completed workflow facts and authenticated sources retained for adapter delivery. */ +export interface ApplicationWorkflowExecution { + readonly evidence: Evidence; + readonly result: Projection; + readonly sources: readonly Evidence[]; +} + +// Validate the request frame without cloning source payloads before their owner reads them. +const featureTraceFrameSchema = z.strictObject({ + ...traceApplicationFeatureInputSchema.shape, + application: z.unknown().nonoptional(), + native_observations: z.array(z.unknown()).default([]), +}); +const versionComparisonFrameSchema = z.strictObject({ + ...compareApplicationVersionsInputSchema.shape, + left: z.unknown().nonoptional(), + right: z.unknown().nonoptional(), + left_native_observations: z.array(z.unknown()).default([]), + right_native_observations: z.array(z.unknown()).default([]), +}); +const exportShapeFrameSchema = z.strictObject({ + ...compareJavaScriptExportShapesInputSchema.shape, + left: z.unknown().nonoptional(), + right: z.unknown().nonoptional(), +}); + +const resolveWorkflowEvidence = ( + raw: unknown, + lookup: EvidenceLookup | undefined, +): Result => { + const reference = retainedEvidenceReferenceSchema.safeParse(raw); + return reference.success + ? resolveEvidenceInput(reference.data, lookup) + : ok(raw); +}; + /** Authenticate Evidence and derive one bounded cross-layer feature trace. */ export const traceApplicationFeatureEvidence = ( rawInput: unknown, -): Result => { + lookup?: EvidenceLookup, +): Result< + ApplicationWorkflowExecution, + AnalysisError +> => { const operation = "trace_application_feature"; - const parsed = traceApplicationFeatureInputSchema.safeParse(rawInput); - if (!parsed.success) + const request = featureTraceFrameSchema.safeParse(rawInput); + if (!request.success) return err( - analysisInputErrorFromIssues(operation, parsed.error.issues, rawInput), - ); - return traceApplicationFeatureEvidenceValidated(parsed.data); -}; - -/** Derive one feature trace from input parsed by a trusted adapter. */ -export const traceApplicationFeatureEvidenceValidated = ( - input: z.output, -): Result => { - const operation = "trace_application_feature"; - const identities = applicationTraceIdentitiesSchema.safeParse(input); - if (!identities.success) - return err( - analysisInputErrorFromIssues(operation, identities.error.issues, input), + analysisInputErrorFromIssues(operation, request.error.issues, rawInput, { + cause: request.error, + }), ); + const selected = resolveWorkflowEvidence(request.data.application, lookup); + if (!selected.ok) return selected; + const input = { ...request.data, application: selected.value }; const sourceResult = parseApplicationGraphEvidence(input.application, [ "application", ]); @@ -73,55 +115,63 @@ export const traceApplicationFeatureEvidenceValidated = ( return err( applicationGraphEvidenceInputError(operation, nativeResult.error), ); - try { - const source = sourceResult.value; - const nativeEvidence = nativeResult.value; - const result = traceApplicationFeature({ - sourceEvidenceId: source.evidence.evidence_id, - graph: source.graph, - nativeEvidence, + const source = sourceResult.value; + const nativeEvidence = nativeResult.value; + const identities = applicationTraceIdentitiesSchema.safeParse({ + native_observations: nativeEvidence, + }); + if (!identities.success) + return err( + analysisInputErrorFromIssues(operation, identities.error.issues, input), + ); + const result = traceApplicationFeature({ + sourceEvidenceId: source.evidence.evidence_id, + graph: source.graph, + nativeEvidence, + seed: input.seed, + direction: input.direction, + }); + const evidence = createApplicationFeatureTraceEvidence( + { + application_evidence_id: source.evidence.evidence_id, + native_evidence_ids: nativeEvidence.map(({ evidence_id: id }) => id), seed: input.seed, direction: input.direction, - }); - return ok( - createApplicationFeatureTraceEvidence( - { - application_evidence_id: source.evidence.evidence_id, - native_evidence_ids: nativeEvidence.map(({ evidence_id: id }) => id), - seed: input.seed, - direction: input.direction, - }, - result, - ), - ); - } catch (cause: unknown) { - return workflowFailure(operation, cause); - } + }, + result, + ); + return ok({ + evidence, + result, + sources: [source.evidence, ...nativeEvidence], + }); }; /** Authenticate both graphs and derive a tiered cross-version change graph. */ export const compareApplicationVersionsEvidence = ( rawInput: unknown, -): Result => { + lookup?: EvidenceLookup, +): Result< + ApplicationWorkflowExecution, + AnalysisError +> => { const operation = "compare_application_versions"; - const parsed = compareApplicationVersionsInputSchema.safeParse(rawInput); - if (!parsed.success) + const request = versionComparisonFrameSchema.safeParse(rawInput); + if (!request.success) return err( - analysisInputErrorFromIssues(operation, parsed.error.issues, rawInput), - ); - return compareApplicationVersionsEvidenceValidated(parsed.data); -}; - -/** Compare versions from input parsed by a trusted adapter. */ -export const compareApplicationVersionsEvidenceValidated = ( - input: z.output, -): Result => { - const operation = "compare_application_versions"; - const identities = applicationVersionIdentitiesSchema.safeParse(input); - if (!identities.success) - return err( - analysisInputErrorFromIssues(operation, identities.error.issues, input), + analysisInputErrorFromIssues(operation, request.error.issues, rawInput, { + cause: request.error, + }), ); + const selectedLeft = resolveWorkflowEvidence(request.data.left, lookup); + if (!selectedLeft.ok) return selectedLeft; + const selectedRight = resolveWorkflowEvidence(request.data.right, lookup); + if (!selectedRight.ok) return selectedRight; + const input = { + ...request.data, + left: selectedLeft.value, + right: selectedRight.value, + }; const leftResult = parseApplicationGraphEvidence(input.left, ["left"]); if (!leftResult.ok) return err(applicationGraphEvidenceInputError(operation, leftResult.error)); @@ -146,48 +196,58 @@ export const compareApplicationVersionsEvidenceValidated = ( return err( applicationGraphEvidenceInputError(operation, rightNativeResult.error), ); - try { - const left = leftResult.value; - const right = rightResult.value; - const leftNative = leftNativeResult.value; - const rightNative = rightNativeResult.value; - const result = compareJavaScriptApplicationVersions({ - left: { - evidenceId: left.evidence.evidence_id, - rootArtifactSha256: left.rootArtifactSha256, - graph: left.graph, - }, - right: { - evidenceId: right.evidence.evidence_id, - rootArtifactSha256: right.rootArtifactSha256, - graph: right.graph, - }, - leftNativeEvidence: leftNative, - rightNativeEvidence: rightNative, - }); - return ok( - createApplicationVersionComparisonEvidence( - { - left_evidence_id: left.evidence.evidence_id, - right_evidence_id: right.evidence.evidence_id, - left_native_evidence_ids: leftNative.map(({ evidence_id: id }) => id), - right_native_evidence_ids: rightNative.map( - ({ evidence_id: id }) => id, - ), - }, - result, - ), + const left = leftResult.value; + const right = rightResult.value; + const leftNative = leftNativeResult.value; + const rightNative = rightNativeResult.value; + const identities = applicationVersionIdentitiesSchema.safeParse({ + left: left.evidence, + right: right.evidence, + left_native_observations: leftNative, + right_native_observations: rightNative, + }); + if (!identities.success) + return err( + analysisInputErrorFromIssues(operation, identities.error.issues, input), ); - } catch (cause: unknown) { - return workflowFailure(operation, cause); - } + const result = compareJavaScriptApplicationVersions({ + left: { + evidenceId: left.evidence.evidence_id, + rootArtifactSha256: left.rootArtifactSha256, + graph: left.graph, + }, + right: { + evidenceId: right.evidence.evidence_id, + rootArtifactSha256: right.rootArtifactSha256, + graph: right.graph, + }, + leftNativeEvidence: leftNative, + rightNativeEvidence: rightNative, + }); + const evidence = createApplicationVersionComparisonEvidence( + { + left_evidence_id: left.evidence.evidence_id, + right_evidence_id: right.evidence.evidence_id, + left_native_evidence_ids: leftNative.map(({ evidence_id: id }) => id), + right_native_evidence_ids: rightNative.map(({ evidence_id: id }) => id), + }, + result, + ); + return ok({ + evidence, + result, + sources: [left.evidence, right.evidence, ...leftNative, ...rightNative], + }); }; /** Compare source and bundle inputs already parsed by a trusted adapter. */ -export const compareSourceToBundleEvidenceValidated = ( +export const compareSourceToBundleEvidence = ( input: ResolvedSourceToBundleReference & Pick, "application">, -): Result => { +): Result< + ApplicationWorkflowExecution, + AnalysisError +> => { const operation = "compare_source_to_bundle"; const applicationResult = parseApplicationGraphEvidence(input.application, [ "application", @@ -196,31 +256,26 @@ export const compareSourceToBundleEvidenceValidated = ( return err( applicationGraphEvidenceInputError(operation, applicationResult.error), ); - try { - const application = applicationResult.value; - const result = compareSourceToBundle({ - reference: input.reference, - application: { - evidenceId: application.evidence.evidence_id, - rootArtifactSha256: application.rootArtifactSha256, - graph: application.graph, - }, - }); - return ok( - createSourceToBundleComparisonEvidence( - { - reference_root_sha256: input.reference.root_sha256, - application_evidence_id: application.evidence.evidence_id, - ...(input.reference_source === undefined - ? {} - : { reference_source: input.reference_source }), - }, - result, - ), - ); - } catch (cause: unknown) { - return workflowFailure(operation, cause); - } + const application = applicationResult.value; + const result = compareSourceToBundle({ + reference: input.reference, + application: { + evidenceId: application.evidence.evidence_id, + rootArtifactSha256: application.rootArtifactSha256, + graph: application.graph, + }, + }); + const evidence = createSourceToBundleComparisonEvidence( + { + reference_root_sha256: input.reference.root_sha256, + application_evidence_id: application.evidence.evidence_id, + ...(input.reference_source === undefined + ? {} + : { reference_source: input.reference_source }), + }, + result, + ); + return ok({ evidence, result, sources: [application.evidence] }); }; /** @@ -229,26 +284,28 @@ export const compareSourceToBundleEvidenceValidated = ( */ export const compareJavaScriptExportShapesEvidence = ( rawInput: unknown, -): Result => { + lookup?: EvidenceLookup, +): Result< + ApplicationWorkflowExecution, + AnalysisError +> => { const operation = "compare_javascript_export_shapes"; - const parsed = compareJavaScriptExportShapesInputSchema.safeParse(rawInput); - if (!parsed.success) + const request = exportShapeFrameSchema.safeParse(rawInput); + if (!request.success) return err( - analysisInputErrorFromIssues(operation, parsed.error.issues, rawInput), - ); - return compareJavaScriptExportShapesEvidenceValidated(parsed.data); -}; - -/** Compare exact export shapes from input parsed by a trusted adapter. */ -export const compareJavaScriptExportShapesEvidenceValidated = ( - input: z.output, -): Result => { - const operation = "compare_javascript_export_shapes"; - const identities = javaScriptExportShapeIdentitiesSchema.safeParse(input); - if (!identities.success) - return err( - analysisInputErrorFromIssues(operation, identities.error.issues, input), + analysisInputErrorFromIssues(operation, request.error.issues, rawInput, { + cause: request.error, + }), ); + const selectedLeft = resolveWorkflowEvidence(request.data.left, lookup); + if (!selectedLeft.ok) return selectedLeft; + const selectedRight = resolveWorkflowEvidence(request.data.right, lookup); + if (!selectedRight.ok) return selectedRight; + const input = { + ...request.data, + left: selectedLeft.value, + right: selectedRight.value, + }; const leftResult = parseApplicationGraphEvidence(input.left, ["left"]); if (!leftResult.ok) return err(applicationGraphEvidenceInputError(operation, leftResult.error)); @@ -257,48 +314,40 @@ export const compareJavaScriptExportShapesEvidenceValidated = ( return err( applicationGraphEvidenceInputError(operation, rightResult.error), ); - try { - const left = leftResult.value; - const right = rightResult.value; - const result = compareJavaScriptExportShapes({ - left: { - evidenceId: left.evidence.evidence_id, - graph: left.graph, - modulePath: input.left_module_path, - exportName: input.left_export_name, - }, - right: { - evidenceId: right.evidence.evidence_id, - graph: right.graph, - modulePath: input.right_module_path, - exportName: input.right_export_name, - }, - }); - return ok( - createJavaScriptExportShapeComparisonEvidence( - { - left_evidence_id: left.evidence.evidence_id, - right_evidence_id: right.evidence.evidence_id, - left_module_path: input.left_module_path, - left_export_name: input.left_export_name, - right_module_path: input.right_module_path, - right_export_name: input.right_export_name, - }, - result, - ), + const left = leftResult.value; + const right = rightResult.value; + const identities = javaScriptExportShapeIdentitiesSchema.safeParse({ + left: left.evidence, + right: right.evidence, + }); + if (!identities.success) + return err( + analysisInputErrorFromIssues(operation, identities.error.issues, input), ); - } catch (cause: unknown) { - return workflowFailure(operation, cause); - } -}; - -const workflowFailure = ( - operation: string, - cause: unknown, -): Result => - err( - new AnalysisProtocolError( - `JavaScript application workflow failed while producing ${operation}`, - { cause }, - ), + const result = compareJavaScriptExportShapes({ + left: { + evidenceId: left.evidence.evidence_id, + graph: left.graph, + modulePath: input.left_module_path, + exportName: input.left_export_name, + }, + right: { + evidenceId: right.evidence.evidence_id, + graph: right.graph, + modulePath: input.right_module_path, + exportName: input.right_export_name, + }, + }); + const evidence = createJavaScriptExportShapeComparisonEvidence( + { + left_evidence_id: left.evidence.evidence_id, + right_evidence_id: right.evidence.evidence_id, + left_module_path: input.left_module_path, + left_export_name: input.left_export_name, + right_module_path: input.right_module_path, + right_export_name: input.right_export_name, + }, + result, ); + return ok({ evidence, result, sources: [left.evidence, right.evidence] }); +}; diff --git a/src/cli/applicationCommands.ts b/src/cli/applicationCommands.ts index ec6fdb0f70..4cc918b767 100644 --- a/src/cli/applicationCommands.ts +++ b/src/cli/applicationCommands.ts @@ -8,11 +8,8 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import type { JsonValue } from "../domain/jsonValue.js"; import type { Logger } from "pino"; -import { traceApplicationFeatureInputSchema } from "../domain/javascript/javascriptFeatureTraceSchemas.js"; import { traceJavaScriptSemanticsInputSchema } from "../domain/javascript/javascriptSemanticTraceSchemas.js"; -import { compareApplicationVersionsInputSchema } from "../domain/javascript/javascriptApplicationVersionComparisonSchemas.js"; import { registerSourceToBundleCommand } from "./sourceToBundleCommand.js"; -import { compareJavaScriptExportShapesInputSchema } from "../domain/javascript/javascriptExportShapeComparisonSchemas.js"; import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; type CliInstance = ReturnType; @@ -28,11 +25,12 @@ export const registerApplicationCommands = ( name: CLI_COMMANDS.traceApplicationFeature, description: "Trace a typed seed through authenticated application Evidence JSON", - inputSchema: traceApplicationFeatureInputSchema, + inputSchema: z.unknown(), workflow: async (input) => { - const { traceApplicationFeatureEvidenceValidated } = + const { traceApplicationFeatureEvidence } = await import("../application/javascript/JavaScriptApplicationWorkflowService.js"); - return traceApplicationFeatureEvidenceValidated(input); + const result = traceApplicationFeatureEvidence(input); + return result.ok ? { ok: true, value: result.value.evidence } : result; }, }); registerJsonCommand({ @@ -54,11 +52,12 @@ export const registerApplicationCommands = ( name: CLI_COMMANDS.compareApplicationVersions, description: "Compare two authenticated JavaScript Application Graph versions", - inputSchema: compareApplicationVersionsInputSchema, + inputSchema: z.unknown(), workflow: async (input) => { - const { compareApplicationVersionsEvidenceValidated } = + const { compareApplicationVersionsEvidence } = await import("../application/javascript/JavaScriptApplicationWorkflowService.js"); - return compareApplicationVersionsEvidenceValidated(input); + const result = compareApplicationVersionsEvidence(input); + return result.ok ? { ok: true, value: result.value.evidence } : result; }, }); registerSourceToBundleCommand(cli, logger); @@ -68,11 +67,12 @@ export const registerApplicationCommands = ( name: CLI_COMMANDS.compareJavaScriptExportShapes, description: "Compare exact static JavaScript export return shapes without execution", - inputSchema: compareJavaScriptExportShapesInputSchema, + inputSchema: z.unknown(), workflow: async (input) => { - const { compareJavaScriptExportShapesEvidenceValidated } = + const { compareJavaScriptExportShapesEvidence } = await import("../application/javascript/JavaScriptApplicationWorkflowService.js"); - return compareJavaScriptExportShapesEvidenceValidated(input); + const result = compareJavaScriptExportShapesEvidence(input); + return result.ok ? { ok: true, value: result.value.evidence } : result; }, }); registerObligationLedgerCommand(cli, logger); diff --git a/src/cli/sourceToBundleCommand.ts b/src/cli/sourceToBundleCommand.ts index 39eb4e0d5a..82933714ec 100644 --- a/src/cli/sourceToBundleCommand.ts +++ b/src/cli/sourceToBundleCommand.ts @@ -53,12 +53,15 @@ export const registerSourceToBundleCommand = ( resources, ); if (!reference.ok) return reference; - const { compareSourceToBundleEvidenceValidated } = + const { compareSourceToBundleEvidence } = await import("../application/javascript/JavaScriptApplicationWorkflowService.js"); - return compareSourceToBundleEvidenceValidated({ + const compared = compareSourceToBundleEvidence({ ...reference.value, application: parsed.data.application, }); + return compared.ok + ? { ok: true, value: compared.value.evidence } + : compared; }, resources, "javascript-application-workflow", diff --git a/src/server/registerApplicationTools/compareExportShapes.ts b/src/server/registerApplicationTools/compareExportShapes.ts index 7380402298..6032d10639 100644 --- a/src/server/registerApplicationTools/compareExportShapes.ts +++ b/src/server/registerApplicationTools/compareExportShapes.ts @@ -1,11 +1,9 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; import { runAdmittedToolOperation } from "../admittedToolOperation.js"; -import { resolvePairedEvidenceRequest } from "../../application/EvidenceInputResolver.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; -import { compareJavaScriptExportShapesEvidenceValidated } from "../../application/javascript/JavaScriptApplicationWorkflowService.js"; +import { compareJavaScriptExportShapesEvidence } from "../../application/javascript/JavaScriptApplicationWorkflowService.js"; import { applicationToolContract } from "../../contracts/applicationToolContracts.js"; -import { javaScriptExportShapeComparisonResultSchema } from "../../domain/javascript/javascriptExportShapeComparisonSchemas.js"; import { logToolExecution } from "../toolLogging.js"; import { toolRegistrationOptions } from "../toolRegistrationOptions.js"; import { recordResult } from "./helpers.js"; @@ -28,39 +26,33 @@ export const registerCompareJavaScriptExportShapesTool = ( contract.name, context.mcpReq.signal, async () => { - const resolved = resolvePairedEvidenceRequest( - input, - options.evidenceById, - ); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, contract); - const parsed = resolved.value; const result = await logToolExecution( options.logger, contract.name, () => Promise.resolve( - compareJavaScriptExportShapesEvidenceValidated(parsed), + compareJavaScriptExportShapesEvidence( + input, + options.evidenceById, + ), ), ); if (!result.ok) return server.delivery.toCallToolResult(result, contract); const recorded = recordSessionEvidenceSources( options.recordEvidence, - [parsed.left, parsed.right], + result.value.sources, ); if (!recorded.ok) return server.delivery.toCallToolResult(recorded, contract); - const comparison = javaScriptExportShapeComparisonResultSchema.parse( - result.value.normalized_result, - ); + const comparison = result.value.result; const unknown = comparison.summary.unknown > 0 || comparison.coverage.status !== "complete-within-inputs"; return recordResult( { ...options, delivery: server.delivery }, contract, - result.value, + result.value.evidence, unknown ? "javascript-export-shape" : undefined, ); }, diff --git a/src/server/registerApplicationTools/compareSourceToBundle.ts b/src/server/registerApplicationTools/compareSourceToBundle.ts index 30a766e124..106fc053af 100644 --- a/src/server/registerApplicationTools/compareSourceToBundle.ts +++ b/src/server/registerApplicationTools/compareSourceToBundle.ts @@ -11,9 +11,8 @@ import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; -import { compareSourceToBundleEvidenceValidated } from "../../application/javascript/JavaScriptApplicationWorkflowService.js"; +import { compareSourceToBundleEvidence } from "../../application/javascript/JavaScriptApplicationWorkflowService.js"; import { applicationToolContract } from "../../contracts/applicationToolContracts.js"; -import { sourceToBundleComparisonResultSchema } from "../../domain/javascript/sourceToBundleComparisonSchemas.js"; import { logToolExecution } from "../toolLogging.js"; import { toolRegistrationOptions } from "../toolRegistrationOptions.js"; import { recordApplicationResult } from "./helpers.js"; @@ -64,15 +63,12 @@ export const registerCompareSourceToBundleTool = ( const result = await logToolExecution( options.logger, contract.name, - () => - Promise.resolve(compareSourceToBundleEvidenceValidated(parsed)), + () => Promise.resolve(compareSourceToBundleEvidence(parsed)), ); if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const parent = result.value; - const comparison = sourceToBundleComparisonResultSchema.parse( - parent.normalized_result, - ); + const parent = result.value.evidence; + const comparison = result.value.result; // Stage the projection against the owned parent before committing any session facts. const selected = input.view === undefined @@ -102,7 +98,7 @@ export const registerCompareSourceToBundleTool = ( return server.delivery.toCallToolResult(selected, contract); const recorded = recordSessionEvidenceSources( options.recordEvidence, - [parsed.application], + result.value.sources, ); if (!recorded.ok) return server.delivery.toCallToolResult(recorded, contract); diff --git a/src/server/registerApplicationTools/compareVersions.ts b/src/server/registerApplicationTools/compareVersions.ts index 7492791406..9780a78150 100644 --- a/src/server/registerApplicationTools/compareVersions.ts +++ b/src/server/registerApplicationTools/compareVersions.ts @@ -1,11 +1,9 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; import { runAdmittedToolOperation } from "../admittedToolOperation.js"; -import { resolvePairedEvidenceRequest } from "../../application/EvidenceInputResolver.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; -import { compareApplicationVersionsEvidenceValidated } from "../../application/javascript/JavaScriptApplicationWorkflowService.js"; +import { compareApplicationVersionsEvidence } from "../../application/javascript/JavaScriptApplicationWorkflowService.js"; import { applicationToolContract } from "../../contracts/applicationToolContracts.js"; -import { applicationVersionComparisonResultSchema } from "../../domain/javascript/javascriptApplicationVersionComparisonSchemas.js"; import { logToolExecution } from "../toolLogging.js"; import { toolRegistrationOptions } from "../toolRegistrationOptions.js"; import { recordResult } from "./helpers.js"; @@ -28,43 +26,27 @@ export const registerCompareApplicationVersionsTool = ( compareContract.name, context.mcpReq.signal, async () => { - const resolved = resolvePairedEvidenceRequest( - input, - options.evidenceById, - ); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, compareContract); - const parsed = resolved.value; const result = await logToolExecution( options.logger, compareContract.name, () => Promise.resolve( - compareApplicationVersionsEvidenceValidated(parsed), + compareApplicationVersionsEvidence(input, options.evidenceById), ), ); if (!result.ok) return server.delivery.toCallToolResult(result, compareContract); - const sources = [ - parsed.left, - parsed.right, - ...parsed.left_native_observations, - ...parsed.right_native_observations, - ]; const recorded = recordSessionEvidenceSources( options.recordEvidence, - sources, + result.value.sources, ); if (!recorded.ok) return server.delivery.toCallToolResult(recorded, compareContract); - const comparison = applicationVersionComparisonResultSchema.parse( - result.value.normalized_result, - ); - const unknown = comparison.summary.unknown > 0; + const unknown = result.value.result.summary.unknown > 0; return recordResult( { ...options, delivery: server.delivery }, compareContract, - result.value, + result.value.evidence, unknown ? "application-version-comparison" : undefined, ); }, diff --git a/src/server/registerApplicationTools/traceFeature.ts b/src/server/registerApplicationTools/traceFeature.ts index 859a3795e4..c774b7a403 100644 --- a/src/server/registerApplicationTools/traceFeature.ts +++ b/src/server/registerApplicationTools/traceFeature.ts @@ -1,9 +1,8 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; import { runAdmittedToolOperation } from "../admittedToolOperation.js"; -import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; -import { traceApplicationFeatureEvidenceValidated } from "../../application/javascript/JavaScriptApplicationWorkflowService.js"; +import { traceApplicationFeatureEvidence } from "../../application/javascript/JavaScriptApplicationWorkflowService.js"; import { applicationToolContract } from "../../contracts/applicationToolContracts.js"; import { logToolExecution } from "../toolLogging.js"; import { toolRegistrationOptions } from "../toolRegistrationOptions.js"; @@ -27,32 +26,26 @@ export const registerTraceFeatureTool = ( traceContract.name, context.mcpReq.signal, async () => { - const resolved = resolveApplicationEvidenceRequest( - input, - options.evidenceById, - ); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, traceContract); - const parsed = resolved.value; const result = await logToolExecution( options.logger, traceContract.name, () => - Promise.resolve(traceApplicationFeatureEvidenceValidated(parsed)), + Promise.resolve( + traceApplicationFeatureEvidence(input, options.evidenceById), + ), ); if (!result.ok) return server.delivery.toCallToolResult(result, traceContract); - const sources = [parsed.application, ...parsed.native_observations]; const recorded = recordSessionEvidenceSources( options.recordEvidence, - sources, + result.value.sources, ); if (!recorded.ok) return server.delivery.toCallToolResult(recorded, traceContract); return recordResult( { ...options, delivery: server.delivery }, traceContract, - result.value, + result.value.evidence, ); }, ), diff --git a/tests/boundary/cli/workflowInputIdentities.test.ts b/tests/boundary/cli/workflowInputIdentities.test.ts index a724e7fea2..125e32a069 100644 --- a/tests/boundary/cli/workflowInputIdentities.test.ts +++ b/tests/boundary/cli/workflowInputIdentities.test.ts @@ -206,6 +206,41 @@ const checkNativeObservationIdentities = async ({ ); }; +const checkMalformedEvidenceBeforeIdentities = async ({ + cli, + root, + application, +}: WorkflowContext) => { + const malformed = { + ...application, + provider: { ...application.provider, name: 3 }, + }; + for (const [command, input, path] of [ + [ + "compare-application-versions", + { left: application, right: malformed }, + ["right", "provider", "name"], + ], + [ + "compare-javascript-export-shapes", + { left: application, right: malformed, ...selectors }, + ["right", "provider", "name"], + ], + [ + "trace-application-feature", + { application, seed, native_observations: [malformed, application] }, + ["native_observations", 0, "provider", "name"], + ], + ] as const) { + const rejected = await runJson(cli, root, command, input); + expect(rejected.exitCode).toBe(1); + expect(rejected.json).toMatchObject({ + code: "invalid_request", + details: { issues: [{ path, reason: "invalid_type" }] }, + }); + } +}; + cliTest( "preserves workflow identity validation through CLI and inline, retained and mixed MCP inputs", async ({ cli }) => { @@ -277,6 +312,7 @@ cliTest( }; await checkComparisonIdentities(context); await checkNativeObservationIdentities(context); + await checkMalformedEvidenceBeforeIdentities(context); const compared = await call("compare_application_versions", { left: reference, right: otherReference, diff --git a/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts b/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts index a757ec941f..00b38506de 100644 --- a/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts +++ b/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts @@ -198,9 +198,7 @@ describe("complete comparison projections", () => { ]); const compared = compareApplicationVersionsEvidence({ left, right }); if (!compared.ok) throw compared.error; - const result = applicationVersionComparisonResultSchema.parse( - compared.value.normalized_result, - ); + const result = compared.value.result; expectFullComparisonResultSchema(result); const modules = result.items.filter( ({ node_kind: kind }) => kind === "javascript-module", @@ -261,9 +259,7 @@ describe("complete comparison projections", () => { ]); const compared = compareApplicationVersionsEvidence({ left, right }); if (!compared.ok) throw compared.error; - const result = applicationVersionComparisonResultSchema.parse( - compared.value.normalized_result, - ); + const result = compared.value.result; expect(result.items).toContainEqual( expect.objectContaining({ node_kind: "source-module", diff --git a/tests/boundary/filesystem/javascriptExportContainerCoverage.test.ts b/tests/boundary/filesystem/javascriptExportContainerCoverage.test.ts index 3fa7163071..8dbb42b1d6 100644 --- a/tests/boundary/filesystem/javascriptExportContainerCoverage.test.ts +++ b/tests/boundary/filesystem/javascriptExportContainerCoverage.test.ts @@ -5,11 +5,7 @@ import { describe, expect, it } from "vitest"; import { parseApplicationGraphEvidence } from "../../../src/application/javascript/JavaScriptApplicationEvidenceGraph.js"; import { analyzeJavaScriptApplication } from "../../support/javascriptApplicationScope.js"; import { compareJavaScriptExportShapesEvidence } from "../../../src/application/javascript/JavaScriptApplicationWorkflowService.js"; -import { parseEvidence } from "../../../src/domain/evidence.js"; -import { - javaScriptExportShapeComparisonResultSchema, - projectedExportReturnShapesSchema, -} from "../../../src/domain/javascript/javascriptExportShapeComparisonSchemas.js"; +import { projectedExportReturnShapesSchema } from "../../../src/domain/javascript/javascriptExportShapeComparisonSchemas.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js"; @@ -267,10 +263,8 @@ const compareEvidence = (left: Source, right: Source) => { right_export_name: "default", }); if (!compared.ok) throw compared.error; - const evidence = parseEvidence(compared.value); - const result = javaScriptExportShapeComparisonResultSchema.parse( - evidence.normalized_result, - ); + const evidence = compared.value.evidence; + const result = compared.value.result; const evidenceLinks = [left.evidence.evidence_id, right.evidence.evidence_id]; expect(evidence).toMatchObject({ operation: "compare_javascript_export_shapes", diff --git a/tests/boundary/mcp/applicationExportShapeMcp.test.ts b/tests/boundary/mcp/applicationExportShapeMcp.test.ts index 4252bbe489..dbd55510bf 100644 --- a/tests/boundary/mcp/applicationExportShapeMcp.test.ts +++ b/tests/boundary/mcp/applicationExportShapeMcp.test.ts @@ -9,7 +9,6 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { analyzeJavaScriptApplication } from "../../support/javascriptApplicationScope.js"; import { compareJavaScriptExportShapesEvidence } from "../../../src/application/javascript/JavaScriptApplicationWorkflowService.js"; import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; -import { javaScriptExportShapeComparisonResultSchema } from "../../../src/domain/javascript/javascriptExportShapeComparisonSchemas.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { APPLICATION_TOOL_CONTRACTS } from "../../../src/contracts/applicationToolContracts.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; @@ -516,7 +515,5 @@ const compareEvidence = ( right_export_name: "default", }); if (!compared.ok) throw compared.error; - return javaScriptExportShapeComparisonResultSchema.parse( - compared.value.normalized_result, - ); + return compared.value.result; }; diff --git a/tests/composition/analysis-sessions/managedApplicationGraph.test.ts b/tests/composition/analysis-sessions/managedApplicationGraph.test.ts index 1233100d65..a766fbf792 100644 --- a/tests/composition/analysis-sessions/managedApplicationGraph.test.ts +++ b/tests/composition/analysis-sessions/managedApplicationGraph.test.ts @@ -209,7 +209,7 @@ describe("managed application graph projection", () => { true, ); if (!trace.ok) throw new Error("trace failed"); - expect(trace.value.normalized_result).toMatchObject({ + expect(trace.value.result).toMatchObject({ source_evidence_id: parsed.evidence_id, summary: { matched_seeds: 1 }, }); diff --git a/tests/process-global/derivedOperationFailures.test.ts b/tests/process-global/derivedOperationFailures.test.ts index ea7229c935..0f03c86eb7 100644 --- a/tests/process-global/derivedOperationFailures.test.ts +++ b/tests/process-global/derivedOperationFailures.test.ts @@ -39,6 +39,16 @@ import { processCaptureSchema } from "../../src/domain/process/processCapture.js import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js"; import { javascriptApplicationAnalysisResultSchema } from "../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { javaScriptSemanticQueryResultSchema } from "../../src/domain/javascript/javascriptSemanticQuerySchemas.js"; +import { + JAVASCRIPT_FEATURE_TRACE_EXAMPLE, + JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE, + SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE, +} from "../../src/contracts/javascript/javascriptApplicationWorkflowExamples.js"; +import { JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE } from "../../src/contracts/javascript/javascriptExportShapeComparisonExample.js"; +import { applicationFeatureTraceResultSchema } from "../../src/domain/javascript/javascriptFeatureTraceSchemas.js"; +import { ownedApplicationVersionComparisonResultSchema } from "../../src/domain/javascript/javascriptApplicationVersionComparisonSchemas.js"; +import { javaScriptExportShapeComparisonResultSchema } from "../../src/domain/javascript/javascriptExportShapeComparisonSchemas.js"; +import { sourceToBundleComparisonResultSchema } from "../../src/domain/javascript/sourceToBundleComparisonSchemas.js"; import { nativeFunctionMatchingSchema } from "../../src/domain/native/functionMatching.js"; import { FunctionByteCoverage } from "../../src/domain/native/functionByteCoverage.js"; @@ -72,6 +82,50 @@ const unexpectedResultFailures = [ ], ] as const; +describe.each([ + { + name: "trace_application_feature", + input: JAVASCRIPT_FEATURE_TRACE_EXAMPLE, + result: applicationFeatureTraceResultSchema, + }, + { + name: "compare_application_versions", + input: JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE, + result: ownedApplicationVersionComparisonResultSchema, + }, + { + name: "compare_javascript_export_shapes", + input: JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE, + result: javaScriptExportShapeComparisonResultSchema, + }, + { + name: "compare_source_to_bundle", + input: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE, + result: sourceToBundleComparisonResultSchema, + }, +])("$name output failures through the public SDK", (workflow) => { + afterEach(() => vi.restoreAllMocks()); + + it.each(unexpectedResultFailures)( + "preserves an unexpected %s without recording sources or derived Evidence", + async (_kind, createFailure) => { + await withSdk(async ({ client, session }) => { + const failure = createFailure(); + vi.spyOn(workflow.result, "parse").mockImplementation(() => { + throw failure; + }); + const response = await client.callTool({ + name: workflow.name, + arguments: workflow.input, + }); + expectUnexpectedToolFailure(response, failure); + expect(session.exportEvidenceBundle().records).toEqual([]); + expect(session.exportEvidenceBundle().unknowns).toEqual([]); + }); + }, + ); +}); + const invalidFunctionOperationEvidence = () => { const left = FUNCTION_COMPARISON_EXAMPLE.left; if (left.subject === null) throw new Error("Expected a function subject"); diff --git a/tests/process-global/sourceToBundleReferenceGuards.test.ts b/tests/process-global/sourceToBundleReferenceGuards.test.ts index d217ee9704..c9f3f82d10 100644 --- a/tests/process-global/sourceToBundleReferenceGuards.test.ts +++ b/tests/process-global/sourceToBundleReferenceGuards.test.ts @@ -122,15 +122,15 @@ it.each(["validation", "comparison"] as const)( const actual = await vi.importActual( "../../src/application/javascript/JavaScriptApplicationWorkflowService.js", ); - vi.mocked( - workflow.compareSourceToBundleEvidenceValidated, - ).mockImplementationOnce((input) => { - const compared = actual.compareSourceToBundleEvidenceValidated(input); - setImmediate(() => - controller.abort(new Error("cancel during graph comparison")), - ); - return compared; - }); + vi.mocked(workflow.compareSourceToBundleEvidence).mockImplementationOnce( + (input) => { + const compared = actual.compareSourceToBundleEvidence(input); + setImmediate(() => + controller.abort(new Error("cancel during graph comparison")), + ); + return compared; + }, + ); } let settle: (() => void) | undefined; const settled = new Promise((resolve) => {