From 24e1fa9a76e447cff318da9f48c7adb659a1f8ab Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:37:54 +0800 Subject: [PATCH] refactor(workflows): simplify evidence reference inputs --- .../ApplicationWorkflowEvidenceResolver.ts | 30 +++----- .../applicationWorkflowInputContracts.ts | 69 ++++--------------- .../javascriptApplicationWorkflowExamples.ts | 4 +- src/contracts/managedWorkflowToolContracts.ts | 34 ++++----- .../applicationEvidenceMcp.test.ts | 21 +++--- .../applicationExportShapeMcp.test.ts | 4 +- .../applicationWorkflowCli.test.ts | 11 ++- .../javascriptApplicationWorkflows.test.ts | 32 ++++++++- .../managedApplicationGraph.test.ts | 21 ++++++ 9 files changed, 107 insertions(+), 119 deletions(-) diff --git a/src/application/ApplicationWorkflowEvidenceResolver.ts b/src/application/ApplicationWorkflowEvidenceResolver.ts index 1328f767fe..e48f84283a 100644 --- a/src/application/ApplicationWorkflowEvidenceResolver.ts +++ b/src/application/ApplicationWorkflowEvidenceResolver.ts @@ -126,23 +126,9 @@ export const resolveCompareApplicationVersionsRequestValidated = ( input: CompareApplicationVersionsRequest, lookup?: EvidenceLookup, ): Result => { - const leftReference = input.left ?? input.left_evidence_id; - if (leftReference === undefined) - return err( - new AnalysisInputError("compare_application_versions", undefined, [ - { path: ["left"], reason: "missing_argument" }, - ]), - ); - const left = resolveApplicationGraphReference(leftReference, lookup); + const left = resolveApplicationGraphReference(input.left, lookup); if (!left.ok) return left; - const rightReference = input.right ?? input.right_evidence_id; - if (rightReference === undefined) - return err( - new AnalysisInputError("compare_application_versions", undefined, [ - { path: ["right"], reason: "missing_argument" }, - ]), - ); - const right = resolveApplicationGraphReference(rightReference, lookup); + const right = resolveApplicationGraphReference(input.right, lookup); if (!right.ok) return right; const leftNative = resolveEvidenceReferences( lookup, @@ -217,16 +203,16 @@ export const resolveCompareJavaScriptExportShapesRequestValidated = ( lookup?: EvidenceLookup, ): Result => { const left = graphEvidence( - "left" in input - ? { application: input.left } - : { application_evidence_id: input.left_evidence_id }, + typeof input.left === "string" + ? { application_evidence_id: input.left } + : { application: input.left }, lookup, ); if (!left.ok) return left; const right = graphEvidence( - "right" in input - ? { application: input.right } - : { application_evidence_id: input.right_evidence_id }, + typeof input.right === "string" + ? { application_evidence_id: input.right } + : { application: input.right }, lookup, ); if (!right.ok) return right; diff --git a/src/contracts/applicationWorkflowInputContracts.ts b/src/contracts/applicationWorkflowInputContracts.ts index 4ad6462131..8b72be9d37 100644 --- a/src/contracts/applicationWorkflowInputContracts.ts +++ b/src/contracts/applicationWorkflowInputContracts.ts @@ -59,37 +59,15 @@ const applicationEvidenceReferenceSchema = z.union([ ]); /** MCP/CLI comparison request accepting inline Evidence or session Evidence IDs. */ -export const compareApplicationVersionsRequestSchema = z - .strictObject({ - ...compareApplicationVersionsFacts, - left: applicationEvidenceReferenceSchema - .optional() - .describe( - "Full application Evidence or an Evidence ID returned this session", - ), - right: applicationEvidenceReferenceSchema - .optional() - .describe( - "Full application Evidence or an Evidence ID returned this session", - ), - // Retain the original ID property names for existing callers. - left_evidence_id: evidenceIdSchema.optional(), - right_evidence_id: evidenceIdSchema.optional(), - }) - .superRefine((input, context) => { - for (const side of ["left", "right"] as const) { - const alias = `${side}_evidence_id` as const; - const hasValue = input[side] !== undefined; - const hasAlias = input[alias] !== undefined; - if (hasValue === hasAlias) { - context.addIssue({ - code: "custom", - path: [side], - message: `Provide exactly one of ${side} or ${alias}`, - }); - } - } - }); +export const compareApplicationVersionsRequestSchema = z.strictObject({ + ...compareApplicationVersionsFacts, + left: applicationEvidenceReferenceSchema.describe( + "Full application Evidence or an Evidence ID returned this session", + ), + right: applicationEvidenceReferenceSchema.describe( + "Full application Evidence or an Evidence ID returned this session", + ), +}); const compareSourceToBundleFacts = { reference: compareSourceToBundleInputSchema.shape.reference, @@ -118,29 +96,12 @@ const compareJavaScriptExportShapesFacts = { compareJavaScriptExportShapesInputSchema.shape.right_export_name, } as const; -/** MCP/CLI export-shape request accepting full Evidence or ledger references. */ -export const compareJavaScriptExportShapesRequestSchema = z.union([ - z.strictObject({ - ...compareJavaScriptExportShapesFacts, - left: evidenceSchema, - right: evidenceSchema, - }), - z.strictObject({ - ...compareJavaScriptExportShapesFacts, - left: evidenceSchema, - right_evidence_id: evidenceIdSchema, - }), - z.strictObject({ - ...compareJavaScriptExportShapesFacts, - left_evidence_id: evidenceIdSchema, - right: evidenceSchema, - }), - z.strictObject({ - ...compareJavaScriptExportShapesFacts, - left_evidence_id: evidenceIdSchema, - right_evidence_id: evidenceIdSchema, - }), -]); +/** MCP/CLI export-shape request accepting full Evidence or session references. */ +export const compareJavaScriptExportShapesRequestSchema = z.strictObject({ + ...compareJavaScriptExportShapesFacts, + left: applicationEvidenceReferenceSchema, + right: applicationEvidenceReferenceSchema, +}); export type TraceApplicationFeatureRequest = z.output< typeof traceApplicationFeatureRequestSchema diff --git a/src/contracts/javascriptApplicationWorkflowExamples.ts b/src/contracts/javascriptApplicationWorkflowExamples.ts index 14e036909e..087536aa3b 100644 --- a/src/contracts/javascriptApplicationWorkflowExamples.ts +++ b/src/contracts/javascriptApplicationWorkflowExamples.ts @@ -111,8 +111,8 @@ export const SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE = { /** Exact static return-shape comparison using producer-returned Evidence IDs. */ export const JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE = { - left_evidence_id: EXAMPLE_EVIDENCE_ID, - right_evidence_id: SECOND_EXAMPLE_EVIDENCE_ID, + left: EXAMPLE_EVIDENCE_ID, + right: SECOND_EXAMPLE_EVIDENCE_ID, left_module_path: "parser.mjs", left_export_name: "default", right_module_path: "parser.mjs", diff --git a/src/contracts/managedWorkflowToolContracts.ts b/src/contracts/managedWorkflowToolContracts.ts index 1733fd9078..f089c2c386 100644 --- a/src/contracts/managedWorkflowToolContracts.ts +++ b/src/contracts/managedWorkflowToolContracts.ts @@ -91,34 +91,24 @@ const managedApplicationGraphReferenceFacts = {} as const; /** MCP references requiring at least one managed Evidence source. */ export const managedApplicationGraphReferenceInputSchema = z - .union([ - z.strictObject({ - ...managedApplicationGraphReferenceFacts, - managed_artifact_evidence_id: managedArtifactEvidenceIdSchema, - managed_members_evidence_id: managedEvidenceIdSchema.optional(), - managed_native_boundaries_evidence_id: - managedBoundaryEvidenceIdSchema.optional(), - }), - z.strictObject({ - ...managedApplicationGraphReferenceFacts, - managed_artifact_evidence_id: managedArtifactEvidenceIdSchema.optional(), - managed_members_evidence_id: managedEvidenceIdSchema, - managed_native_boundaries_evidence_id: - managedBoundaryEvidenceIdSchema.optional(), - }), - z.strictObject({ - ...managedApplicationGraphReferenceFacts, - managed_artifact_evidence_id: managedArtifactEvidenceIdSchema.optional(), - managed_members_evidence_id: managedEvidenceIdSchema.optional(), - managed_native_boundaries_evidence_id: managedBoundaryEvidenceIdSchema, - }), - ]) + .strictObject({ + ...managedApplicationGraphReferenceFacts, + managed_artifact_evidence_id: managedArtifactEvidenceIdSchema.optional(), + managed_members_evidence_id: managedEvidenceIdSchema.optional(), + managed_native_boundaries_evidence_id: + managedBoundaryEvidenceIdSchema.optional(), + }) .superRefine((input, context) => { const ids = [ input.managed_artifact_evidence_id, input.managed_members_evidence_id, input.managed_native_boundaries_evidence_id, ].filter((id): id is string => id !== undefined); + if (ids.length === 0) + context.addIssue({ + code: "custom", + message: "Provide at least one managed Evidence source", + }); if (new Set(ids).size !== ids.length) context.addIssue({ code: "custom", diff --git a/tests/acceptance/applications/applicationEvidenceMcp.test.ts b/tests/acceptance/applications/applicationEvidenceMcp.test.ts index 6faaa789ec..a8adc946f9 100644 --- a/tests/acceptance/applications/applicationEvidenceMcp.test.ts +++ b/tests/acceptance/applications/applicationEvidenceMcp.test.ts @@ -108,9 +108,9 @@ async function runEvidenceIdScenarios(harness: TestHarness): Promise { const comparedById = await harness.client.callTool({ name: "compare_application_versions", arguments: { - left_evidence_id: - JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left.evidence_id, - right_evidence_id: + left: JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left + .evidence_id, + right: JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right.evidence_id, }, }); @@ -145,15 +145,16 @@ async function runEvidenceIdScenarios(harness: TestHarness): Promise { }); expect(mixedRightInline.isError).not.toBe(true); - const mixedWithLegacyAlias = await harness.client.callTool({ + const legacyAliases = await harness.client.callTool({ name: "compare_application_versions", arguments: { - left: JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left, + left_evidence_id: + JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left.evidence_id, right_evidence_id: JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right.evidence_id, }, }); - expect(mixedWithLegacyAlias.isError).not.toBe(true); + expect(legacyAliases.isError).toBe(true); const sourceComparedById = await harness.client.callTool({ name: "compare_source_to_bundle", @@ -243,16 +244,16 @@ async function assertRejectedInlineEvidence(client: Client): Promise { }); expect(spoofed.isError).toBe(true); - const duplicateComparisonReference = await client.callTool({ + const legacyComparisonReferences = await client.callTool({ name: "compare_application_versions", arguments: { - left: JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left, left_evidence_id: JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left.evidence_id, - right: JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right, + right_evidence_id: + JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right.evidence_id, }, }); - expect(duplicateComparisonReference.isError).toBe(true); + expect(legacyComparisonReferences.isError).toBe(true); } describe("application workflow MCP parity", () => { diff --git a/tests/acceptance/applications/applicationExportShapeMcp.test.ts b/tests/acceptance/applications/applicationExportShapeMcp.test.ts index d6ec76e3ff..0d871bad51 100644 --- a/tests/acceptance/applications/applicationExportShapeMcp.test.ts +++ b/tests/acceptance/applications/applicationExportShapeMcp.test.ts @@ -83,8 +83,8 @@ describe("application workflow MCP parity", () => { const byId = await client.callTool({ name: "compare_javascript_export_shapes", arguments: { - left_evidence_id: left.value.evidence_id, - right_evidence_id: right.value.evidence_id, + left: left.value.evidence_id, + right: right.value.evidence_id, ...selectors, }, }); diff --git a/tests/acceptance/applications/applicationWorkflowCli.test.ts b/tests/acceptance/applications/applicationWorkflowCli.test.ts index 678307f94d..bd098ac25d 100644 --- a/tests/acceptance/applications/applicationWorkflowCli.test.ts +++ b/tests/acceptance/applications/applicationWorkflowCli.test.ts @@ -159,10 +159,9 @@ describe("application workflow CLI Evidence resolution", () => { [ "compare-application-versions", JSON.stringify({ - left_evidence_id: - JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left - .evidence_id, - right_evidence_id: + left: JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left + .evidence_id, + right: JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right .evidence_id, }), @@ -251,8 +250,8 @@ describe("application workflow CLI export Evidence", () => { [ "compare-javascript-export-shapes", JSON.stringify({ - left_evidence_id: left.value.evidence_id, - right_evidence_id: right.value.evidence_id, + left: left.value.evidence_id, + right: right.value.evidence_id, left_module_path: "parser.mjs", left_export_name: "default", right_module_path: "parser.mjs", diff --git a/tests/acceptance/applications/javascriptApplicationWorkflows.test.ts b/tests/acceptance/applications/javascriptApplicationWorkflows.test.ts index e4da00f2b5..e4e3dd46d5 100644 --- a/tests/acceptance/applications/javascriptApplicationWorkflows.test.ts +++ b/tests/acceptance/applications/javascriptApplicationWorkflows.test.ts @@ -4,7 +4,10 @@ import { afterEach, describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { compareApplicationVersionsRequestSchema } from "../../../src/contracts/applicationWorkflowInputContracts.js"; +import { + compareApplicationVersionsRequestSchema, + compareJavaScriptExportShapesRequestSchema, +} from "../../../src/contracts/applicationWorkflowInputContracts.js"; import { analyzeJavaScriptApplication } from "../../../src/application/JavaScriptApplicationService.js"; import { compareApplicationVersionsEvidence } from "../../../src/application/JavaScriptApplicationWorkflowService.js"; import { createEvidence } from "../../../src/domain/evidence.js"; @@ -164,6 +167,33 @@ describe("JavaScript application comparison workflows", () => { limits: { max_comparison_items: 1 }, }).success, ).toBe(false); + expect( + compareApplicationVersionsRequestSchema.safeParse({ + left_evidence_id: `ev_${"a".repeat(64)}`, + right_evidence_id: `ev_${"b".repeat(64)}`, + }).success, + ).toBe(false); + }); + + it("uses one consistent Evidence reference shape for export comparisons", () => { + const request = { + left: `ev_${"a".repeat(64)}`, + right: `ev_${"b".repeat(64)}`, + left_module_path: "parser.mjs", + left_export_name: "default", + right_module_path: "parser.mjs", + right_export_name: "default", + }; + expect( + compareJavaScriptExportShapesRequestSchema.safeParse(request).success, + ).toBe(true); + expect( + compareJavaScriptExportShapesRequestSchema.safeParse({ + ...request, + left_evidence_id: request.left, + left: undefined, + }).success, + ).toBe(false); }); }); diff --git a/tests/composition/analysis-sessions/managedApplicationGraph.test.ts b/tests/composition/analysis-sessions/managedApplicationGraph.test.ts index fa850ff625..6b1bfe7f76 100644 --- a/tests/composition/analysis-sessions/managedApplicationGraph.test.ts +++ b/tests/composition/analysis-sessions/managedApplicationGraph.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { projectManagedApplicationGraphEvidence } from "../../../src/application/ManagedApplicationGraphService.js"; +import { managedApplicationGraphReferenceInputSchema } from "../../../src/contracts/managedWorkflowToolContracts.js"; import { traceApplicationFeatureEvidence } from "../../../src/application/JavaScriptApplicationWorkflowService.js"; import { MANAGED_STATIC_PROVIDER } from "../../../src/application/InvestigationProviders.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; @@ -159,6 +160,26 @@ describe("managed application graph projection", () => { }); }); +describe("managed application graph request", () => { + it("accepts any nonempty set of sources and rejects duplicate IDs", () => { + const id = `ev_${"a".repeat(64)}`; + expect( + managedApplicationGraphReferenceInputSchema.safeParse({ + managed_members_evidence_id: id, + }).success, + ).toBe(true); + expect( + managedApplicationGraphReferenceInputSchema.safeParse({}).success, + ).toBe(false); + expect( + managedApplicationGraphReferenceInputSchema.safeParse({ + managed_members_evidence_id: id, + managed_native_boundaries_evidence_id: id, + }).success, + ).toBe(false); + }); +}); + describe("managed application graph coverage", () => { it("preserves partial parser coverage in graph and per-fact coverage", () => { const bytes = buildManagedPeFixture();