diff --git a/src/application/javascript/JavaScriptSemanticTraceService.ts b/src/application/javascript/JavaScriptSemanticTraceService.ts index 0a51a9398d..8aa831599e 100644 --- a/src/application/javascript/JavaScriptSemanticTraceService.ts +++ b/src/application/javascript/JavaScriptSemanticTraceService.ts @@ -1,20 +1,16 @@ import { z } from "zod"; -import { - AnalysisInputError, - AnalysisProtocolError, -} from "../../domain/analysisErrorCore.js"; +import { AnalysisInputError } from "../../domain/analysisErrorCore.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { applicationGraphEvidenceInputError, parseApplicationGraphEvidence, } from "./JavaScriptApplicationEvidenceGraph.js"; import type { Evidence } from "../../domain/evidence.js"; -import { jsonValueSchema } from "../../domain/jsonValue.js"; -import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js"; +import { queryJavaScriptSemanticGraphValidated } from "../../domain/javascript/javascriptSemanticQuery.js"; import { - javaScriptSemanticTraceResultSchema, traceJavaScriptSemanticsInputSchema, + type JavaScriptSemanticTraceResult, } from "../../domain/javascript/javascriptSemanticTraceSchemas.js"; import { err, ok, type Result } from "../../domain/result.js"; import { createJavaScriptSemanticTraceEvidence } from "./JavaScriptApplicationWorkflowEvidence.js"; @@ -44,28 +40,26 @@ export const traceJavaScriptSemanticsEvidenceValidated = ( }, ]), ); - try { - const query = queryJavaScriptSemanticGraph( - source.semanticGraph, - input.query, - ); - const result = javaScriptSemanticTraceResultSchema.parse({ - ...query, - source_evidence_id: source.evidence.evidence_id, - evidence_links: [source.evidence.evidence_id], - }); - return ok( - createJavaScriptSemanticTraceEvidence( - { - application_evidence_id: source.evidence.evidence_id, - query: jsonValueSchema.parse(input.query), - }, - result, - ), - ); - } catch (cause: unknown) { - return err( - new AnalysisProtocolError("JavaScript semantic trace failed", { cause }), - ); - } + const query = queryJavaScriptSemanticGraphValidated( + source.semanticGraph, + input.query, + ); + const result: JavaScriptSemanticTraceResult = { + ...query, + source_evidence_id: source.evidence.evidence_id, + evidence_links: [source.evidence.evidence_id], + }; + const { allowed_relations, ...queryParameters } = input.query; + return ok( + createJavaScriptSemanticTraceEvidence( + { + application_evidence_id: source.evidence.evidence_id, + query: + allowed_relations === undefined + ? queryParameters + : { ...queryParameters, allowed_relations }, + }, + result, + ), + ); }; diff --git a/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts b/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts index ff5238d2b3..785538f3c2 100644 --- a/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts +++ b/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts @@ -4,6 +4,7 @@ import { unknownSemanticGraphTestEvidence, } from "../../../tests/support/javascriptSemanticGraphFixture.js"; import { expect, it } from "vitest"; +import { z } from "zod"; import { JAVASCRIPT_SEMANTIC_NODE_KINDS, @@ -24,7 +25,11 @@ import type { JavaScriptSemanticGraphNode, JavaScriptSemanticGraphRelation, } from "./javascriptSemanticGraphSchemas.js"; -import { queryJavaScriptSemanticGraph } from "./javascriptSemanticQuery.js"; +import { + queryJavaScriptSemanticGraph, + queryJavaScriptSemanticGraphValidated, +} from "./javascriptSemanticQuery.js"; +import { javaScriptSemanticQueryInputSchema } from "./javascriptSemanticQuerySchemas.js"; import type { JsonValue } from "../jsonValue.js"; const SHA = "a".repeat(64); @@ -266,7 +271,10 @@ it("excludes candidate edges unless the caller explicitly opts in", () => { direction: "forward-influence" as const, allowed_relations: ["supplies-request-field" as const], }; - const conservative = queryJavaScriptSemanticGraph(graph, input); + const conservative = queryJavaScriptSemanticGraphValidated( + graph, + javaScriptSemanticQueryInputSchema.parse(input), + ); const optedIn = queryJavaScriptSemanticGraph(graph, { ...input, include_ambiguous_dynamic_edges: true, @@ -281,6 +289,16 @@ it("excludes candidate edges unless the caller explicitly opts in", () => { ]); }); +it("rejects an empty relation selection at the raw query boundary", () => { + expect(() => + queryJavaScriptSemanticGraph(graphWithCandidateEdge(), { + seed: { kind: "literal", value: "TOKEN" }, + direction: "forward-influence", + allowed_relations: [], + }), + ).toThrow(z.ZodError); +}); + it("retains every structural application node linked to one semantic node", () => { const evidenceContexts = new JavaScriptSemanticEvidenceContextRegistry(); const original = node("module", "module", evidenceContexts); diff --git a/src/domain/javascript/javascriptSemanticQuery.ts b/src/domain/javascript/javascriptSemanticQuery.ts index 7b883521aa..7db6689e19 100644 --- a/src/domain/javascript/javascriptSemanticQuery.ts +++ b/src/domain/javascript/javascriptSemanticQuery.ts @@ -38,8 +38,17 @@ interface QueryResult { export const queryJavaScriptSemanticGraph = ( graph: JavaScriptSemanticGraph, rawInput: unknown, +): JavaScriptSemanticQueryResult => + queryJavaScriptSemanticGraphValidated( + graph, + javaScriptSemanticQueryInputSchema.parse(rawInput), + ); + +/** Run a query already parsed by its owning request boundary. */ +export const queryJavaScriptSemanticGraphValidated = ( + graph: JavaScriptSemanticGraph, + input: JavaScriptSemanticQueryInput, ): JavaScriptSemanticQueryResult => { - const input = javaScriptSemanticQueryInputSchema.parse(rawInput); const queryId = queryIdentifier(graph, input); const seeds = resolveSeeds(graph, input); const nodes = new Map(graph.nodes.map((node) => [node.node_id, node])); diff --git a/tests/boundary/javascriptSemanticTraceEvidence.test.ts b/tests/boundary/javascriptSemanticTraceEvidence.test.ts index 94157c1307..7cb46c65f1 100644 --- a/tests/boundary/javascriptSemanticTraceEvidence.test.ts +++ b/tests/boundary/javascriptSemanticTraceEvidence.test.ts @@ -297,6 +297,22 @@ const assertSuccessfulTraceParity = async ( const cliResult = javaScriptSemanticTraceResultSchema.parse( cliEvidence.normalized_result, ); + expect(cliEvidence.parameters.query).toMatchObject({ + seed: query.seed, + direction: query.direction, + include_ambiguous_dynamic_edges: false, + expected: null, + }); + if (query.allowed_relations === undefined) + expect(cliEvidence.parameters.query).not.toHaveProperty( + "allowed_relations", + ); + else + expect(cliEvidence.parameters.query).toHaveProperty( + "allowed_relations", + query.allowed_relations, + ); + assertTraceContextsResolve(cliResult); const mcpResponse = await harness.client.callTool({ diff --git a/tests/process-global/derivedOperationFailures.test.ts b/tests/process-global/derivedOperationFailures.test.ts index fb151326b6..482013fdae 100644 --- a/tests/process-global/derivedOperationFailures.test.ts +++ b/tests/process-global/derivedOperationFailures.test.ts @@ -36,6 +36,9 @@ import { processEvidence, } from "../../src/application/ReconstructionObligationLedger.fixture.js"; 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"; /** Unexpected implementation failures retain the SDK text surface, without a typed caller error. */ const expectUnexpectedToolFailure = ( @@ -468,3 +471,41 @@ describe("ledger generation failure ownership through the public SDK", () => { }, ); }); + +describe("semantic query output failures through the public SDK", () => { + afterEach(() => vi.restoreAllMocks()); + + it.each(unexpectedResultFailures)( + "preserves an unexpected %s from generated query validation", + async (_label, createFailure) => { + await withSdk(async ({ client, session }) => { + const application = JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE; + const analysis = javascriptApplicationAnalysisResultSchema.parse( + application.normalized_result, + ); + const node = analysis.semantic_graph.nodes[0]; + if (node === undefined) + throw new Error("Expected a semantic fixture node"); + const failure = createFailure(); + vi.spyOn( + javaScriptSemanticQueryResultSchema, + "parse", + ).mockImplementation(() => { + throw failure; + }); + const response = await client.callTool({ + name: "trace_javascript_semantics", + arguments: { + application, + query: { + seed: { kind: "semantic-node", node_id: node.node_id }, + direction: "backward-provenance", + }, + }, + }); + expectUnexpectedToolFailure(response, failure); + expect(session.exportEvidenceBundle().records).toEqual([]); + }); + }, + ); +});