fix: report malformed application evidence as input errors

This commit is contained in:
morluto
2026-10-10 03:52:35 +08:00
parent 55a46f7c71
commit 218c13160e
15 changed files with 613 additions and 165 deletions
@@ -76,7 +76,14 @@ const deriveApplicationCandidates = (
)
return;
try {
const source = parseApplicationGraphEvidence(evidence);
const parsed = parseApplicationGraphEvidence(evidence);
if (!parsed.ok) {
limitations.add(
`Application Evidence ${evidence.evidence_id} could not generate candidates because it failed graph validation.`,
);
return;
}
const source = parsed.value;
for (const node of source.graph.nodes) {
const candidatePolicy = applicationObligationPolicy(node.kind);
if (candidatePolicy === undefined) {
@@ -8,6 +8,13 @@ import {
parseEvidence,
type Evidence,
} from "../../domain/evidence.js";
import {
type AnalysisInputIssue,
AnalysisInputError,
} from "../../domain/analysisErrorCore.js";
import { projectInputIssues } from "../../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../../domain/result.js";
import { z } from "zod";
import {
analyzeJavaScriptApplicationInputSchema,
javascriptApplicationAnalysisResultSchema,
@@ -30,6 +37,24 @@ export interface ApplicationGraphEvidenceSource {
readonly semanticGraph: JavaScriptSemanticGraph | null;
}
/** Caller-owned Evidence failed validation at the application graph boundary. */
export interface ApplicationGraphEvidenceInputFailure {
readonly issues: readonly AnalysisInputIssue[];
}
/** Convert typed parser issues into this workflow's caller-facing input error. */
export const applicationGraphEvidenceInputError = (
operation: string,
failure: ApplicationGraphEvidenceInputFailure,
): AnalysisInputError =>
new AnalysisInputError(operation, undefined, failure.issues);
class InvalidApplicationGraphEvidence extends Error {
constructor(readonly issues: readonly AnalysisInputIssue[]) {
super("Application graph Evidence failed input validation");
}
}
const ownedApplicationSources = new WeakMap<
Evidence,
ApplicationGraphEvidenceSource
@@ -51,7 +76,7 @@ export const rememberOwnedApplicationGraphEvidence = (
);
ownedApplicationSources.set(
evidence,
Object.freeze(staticApplicationSourceForResult(evidence, result)),
Object.freeze(staticApplicationSourceForResult(evidence, result, [])),
);
return evidence;
};
@@ -59,62 +84,79 @@ export const rememberOwnedApplicationGraphEvidence = (
/** Parse and authenticate one REA-produced JavaScript Application Graph Evidence. */
export const parseApplicationGraphEvidence = (
input: unknown,
): ApplicationGraphEvidenceSource => {
const evidence = parseEvidence(input);
const owned = ownedApplicationSources.get(evidence);
if (owned !== undefined) return owned;
if (
evidence.operation === "analyze_javascript_application" &&
evidence.predicate_type === "rea.javascript-application-analysis" &&
providerMatches(evidence, JAVASCRIPT_APPLICATION_PROVIDER)
)
return staticApplicationSource(evidence);
if (
evidence.operation === "reconcile_javascript_runtime" &&
evidence.predicate_type === "rea.javascript-runtime-reconciliation" &&
providerMatches(evidence, JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER)
)
return staticRuntimeSource(evidence);
if (
evidence.operation === "project_managed_application_graph" &&
evidence.predicate_type === "rea.managed-application-graph" &&
providerMatches(evidence, MANAGED_WORKFLOW_PROVIDER)
)
return managedApplicationSource(evidence);
throw new TypeError(
"Application workflow requires authenticated analyze_javascript_application, reconcile_javascript_runtime, or project_managed_application_graph Evidence",
);
};
path: readonly (string | number)[] = [],
): Result<
ApplicationGraphEvidenceSource,
ApplicationGraphEvidenceInputFailure
> =>
captureInputValidation(() => {
const evidence = parseEvidenceInput(input, path);
const owned = ownedApplicationSources.get(evidence);
if (owned !== undefined) return owned;
if (evidence.operation === "analyze_javascript_application") {
requirePredicate(evidence, "rea.javascript-application-analysis", path);
requireProvider(evidence, JAVASCRIPT_APPLICATION_PROVIDER, path);
return staticApplicationSource(evidence, path);
}
if (evidence.operation === "reconcile_javascript_runtime") {
requirePredicate(evidence, "rea.javascript-runtime-reconciliation", path);
requireProvider(
evidence,
JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER,
path,
);
return staticRuntimeSource(evidence, path);
}
if (evidence.operation === "project_managed_application_graph") {
requirePredicate(evidence, "rea.managed-application-graph", path);
requireProvider(evidence, MANAGED_WORKFLOW_PROVIDER, path);
return managedApplicationSource(evidence, path);
}
throw invalidEvidence(
path,
"operation",
"Expected authenticated JavaScript analysis, runtime reconciliation, or managed application graph Evidence.",
);
});
const staticApplicationSource = (
evidence: Evidence,
path: readonly (string | number)[],
): ApplicationGraphEvidenceSource => {
const result = javascriptApplicationAnalysisResultSchema.parse(
const result = parseCallerValue(
(input) => javascriptApplicationAnalysisResultSchema.parse(input),
evidence.normalized_result,
[...path, "normalized_result"],
);
return staticApplicationSourceForResult(evidence, result);
return staticApplicationSourceForResult(evidence, result, path);
};
const staticApplicationSourceForResult = (
evidence: Evidence,
result: JavaScriptApplicationAnalysisResult,
path: readonly (string | number)[],
): ApplicationGraphEvidenceSource => {
if (
evidence.authority !== "shipped-artifact" ||
evidence.confidence !== "derived"
)
throw new TypeError(
"JavaScript application Evidence authority or confidence is invalid",
throw invalidEvidence(
path,
"authority",
"JavaScript application Evidence must use shipped-artifact authority and derived confidence.",
);
if (evidence.predicate_type !== "rea.javascript-application-analysis")
throw new TypeError(
"JavaScript application Evidence predicate does not match its result shape",
throw invalidEvidence(
path,
"predicate_type",
"JavaScript application Evidence predicate does not match its result shape.",
);
analyzeJavaScriptApplicationInputSchema.parse({
input_path: result.input_path,
...evidence.parameters,
});
assertApplicationSubject(evidence, result);
parseCallerValue(
(input) => analyzeJavaScriptApplicationInputSchema.parse(input),
{ input_path: result.input_path, ...evidence.parameters },
[...path, "parameters"],
);
assertApplicationSubject(evidence, result, path);
return {
evidence,
graph: result.graph,
@@ -126,30 +168,40 @@ const staticApplicationSourceForResult = (
const staticRuntimeSource = (
evidence: Evidence,
path: readonly (string | number)[],
): ApplicationGraphEvidenceSource => {
if (
evidence.authority !== "analyst-inference" ||
evidence.confidence !== "inferred"
)
throw new TypeError(
"JavaScript runtime reconciliation Evidence authority or confidence is invalid",
throw invalidEvidence(
path,
"authority",
"Runtime reconciliation Evidence must use analyst-inference authority and inferred confidence.",
);
const result = javascriptRuntimeReconciliationResultSchema.parse(
const result = parseCallerValue(
(input) => javascriptRuntimeReconciliationResultSchema.parse(input),
evidence.normalized_result,
[...path, "normalized_result"],
);
if (
result.evidence_links.some(
(evidenceId) => !evidence.evidence_links.includes(evidenceId),
)
)
throw new TypeError(
"Runtime reconciliation result references Evidence outside its envelope",
const unmatchedLink = result.evidence_links.find(
(evidenceId) => !evidence.evidence_links.includes(evidenceId),
);
if (unmatchedLink !== undefined)
throw invalidEvidence(
[...path, "normalized_result"],
"evidence_links",
`Runtime reconciliation result references Evidence ${unmatchedLink} outside its envelope.`,
);
const applicationLayer = result.static_layers.find(
({ role }) => role === "application",
);
if (applicationLayer === undefined)
throw new TypeError("Runtime reconciliation application layer is missing");
throw invalidEvidence(
[...path, "normalized_result"],
"static_layers",
"Runtime reconciliation Evidence must include its application layer.",
);
return {
evidence,
graph: result.graph,
@@ -161,24 +213,30 @@ const staticRuntimeSource = (
const managedApplicationSource = (
evidence: Evidence,
path: readonly (string | number)[],
): ApplicationGraphEvidenceSource => {
if (
evidence.authority !== "analyst-inference" ||
evidence.confidence !== "inferred"
)
throw new TypeError(
"Managed application graph Evidence authority or confidence is invalid",
throw invalidEvidence(
path,
"authority",
"Managed application graph Evidence must use analyst-inference authority and inferred confidence.",
);
const result = managedApplicationGraphResultSchema.parse(
const result = parseCallerValue(
(input) => managedApplicationGraphResultSchema.parse(input),
evidence.normalized_result,
[...path, "normalized_result"],
);
if (
result.evidence_links.some(
(evidenceId) => !evidence.evidence_links.includes(evidenceId),
)
)
throw new TypeError(
"Managed application graph result references Evidence outside its envelope",
const unmatchedLink = result.evidence_links.find(
(evidenceId) => !evidence.evidence_links.includes(evidenceId),
);
if (unmatchedLink !== undefined)
throw invalidEvidence(
[...path, "normalized_result"],
"evidence_links",
`Managed application graph result references Evidence ${unmatchedLink} outside its envelope.`,
);
return {
evidence,
@@ -192,6 +250,7 @@ const managedApplicationSource = (
const assertApplicationSubject = (
evidence: Evidence,
result: JavaScriptApplicationAnalysisResult,
path: readonly (string | number)[],
): void => {
if (
evidence.subject === null ||
@@ -199,8 +258,10 @@ const assertApplicationSubject = (
evidence.subject.format !== result.format ||
evidence.subject.local_path !== result.input_path
)
throw new TypeError(
"JavaScript application Evidence subject does not match its result",
throw invalidEvidence(
path,
"subject",
"JavaScript application Evidence subject must match the analyzed artifact identity and input path.",
);
};
@@ -216,15 +277,116 @@ const providerMatches = (
evidence.provider.name === provider.name &&
evidence.provider.version === provider.version;
const requirePredicate = (
evidence: Evidence,
expected: string,
path: readonly (string | number)[],
): void => {
if (evidence.predicate_type !== expected)
throw invalidEvidence(
path,
"predicate_type",
`Expected predicate ${expected} for operation ${evidence.operation}.`,
);
};
const requireProvider = (
evidence: Evidence,
expected: {
readonly id: string;
readonly name: string;
readonly version: string;
},
path: readonly (string | number)[],
): void => {
if (!providerMatches(evidence, expected))
throw invalidEvidence(
path,
"provider",
`Expected provider ${expected.id} v${expected.version} for operation ${evidence.operation}.`,
);
};
/** Parse unique, artifact-bound Evidence that may extend a native handoff. */
export const parseNativeApplicationEvidence = (
inputs: readonly unknown[],
): Evidence[] => {
const parsed = inputs.map((input) => parseEvidence(input));
if (parsed.some(({ subject }) => subject === null))
throw new TypeError("Native handoff Evidence requires an artifact subject");
const ids = parsed.map(({ evidence_id: id }) => id);
if (new Set(ids).size !== ids.length)
throw new TypeError("Native handoff Evidence must be unique");
return parsed;
path: readonly (string | number)[] = [],
): Result<readonly Evidence[], ApplicationGraphEvidenceInputFailure> =>
captureInputValidation(() => {
const parsed = inputs.map((input, index) =>
parseEvidenceInput(input, [...path, index]),
);
const withoutSubject = parsed.findIndex(({ subject }) => subject === null);
if (withoutSubject !== -1)
throw invalidEvidence(
[...path, withoutSubject],
"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;
});
const captureInputValidation = <Value>(
operation: () => Value,
): Result<Value, ApplicationGraphEvidenceInputFailure> => {
try {
return ok(operation());
} catch (cause: unknown) {
if (cause instanceof InvalidApplicationGraphEvidence)
return err({ issues: cause.issues });
throw cause;
}
};
const parseEvidenceInput = (
input: unknown,
path: readonly (string | number)[],
): Evidence => {
try {
return parseEvidence(input);
} catch (cause: unknown) {
if (cause instanceof z.ZodError) throw zodInputFailure(cause, input, path);
throw cause;
}
};
const parseCallerValue = <Value>(
parse: (input: unknown) => Value,
input: unknown,
path: readonly (string | number)[],
): Value => {
try {
return parse(input);
} catch (cause: unknown) {
if (cause instanceof z.ZodError) throw zodInputFailure(cause, input, path);
throw cause;
}
};
const zodInputFailure = (
cause: z.ZodError,
input: unknown,
path: readonly (string | number)[],
): InvalidApplicationGraphEvidence =>
new InvalidApplicationGraphEvidence(
projectInputIssues(cause.issues, input).map((issue) => ({
...issue,
path: [...path, ...issue.path],
})),
);
const invalidEvidence = (
path: readonly (string | number)[],
field: string,
message: string,
): InvalidApplicationGraphEvidence =>
new InvalidApplicationGraphEvidence([
{ path: [...path, field], reason: "invalid_value", message },
]);
@@ -17,6 +17,7 @@ import { err, ok, type Result } from "../../domain/result.js";
import { compareSourceToBundle } from "../../domain/javascript/sourceToBundleComparison.js";
import { compareSourceToBundleInputSchema } from "../../domain/javascript/sourceToBundleComparisonSchemas.js";
import {
applicationGraphEvidenceInputError,
parseApplicationGraphEvidence,
parseNativeApplicationEvidence,
} from "./JavaScriptApplicationEvidenceGraph.js";
@@ -45,11 +46,24 @@ export const traceApplicationFeatureEvidenceValidated = (
input: z.output<typeof traceApplicationFeatureInputSchema>,
): Result<Evidence, AnalysisError> => {
const operation = "trace_application_feature";
try {
const source = parseApplicationGraphEvidence(input.application);
const nativeEvidence = parseNativeApplicationEvidence(
input.native_observations,
const sourceResult = parseApplicationGraphEvidence(input.application, [
"application",
]);
if (!sourceResult.ok)
return err(
applicationGraphEvidenceInputError(operation, sourceResult.error),
);
const nativeResult = parseNativeApplicationEvidence(
input.native_observations,
["native_observations"],
);
if (!nativeResult.ok)
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,
@@ -69,7 +83,7 @@ export const traceApplicationFeatureEvidenceValidated = (
),
);
} catch (cause: unknown) {
return workflowFailure(operation, cause, input);
return workflowFailure(operation, cause);
}
};
@@ -91,15 +105,35 @@ export const compareApplicationVersionsEvidenceValidated = (
input: z.output<typeof compareApplicationVersionsInputSchema>,
): Result<Evidence, AnalysisError> => {
const operation = "compare_application_versions";
const leftResult = parseApplicationGraphEvidence(input.left, ["left"]);
if (!leftResult.ok)
return err(applicationGraphEvidenceInputError(operation, leftResult.error));
const rightResult = parseApplicationGraphEvidence(input.right, ["right"]);
if (!rightResult.ok)
return err(
applicationGraphEvidenceInputError(operation, rightResult.error),
);
const leftNativeResult = parseNativeApplicationEvidence(
input.left_native_observations,
["left_native_observations"],
);
if (!leftNativeResult.ok)
return err(
applicationGraphEvidenceInputError(operation, leftNativeResult.error),
);
const rightNativeResult = parseNativeApplicationEvidence(
input.right_native_observations,
["right_native_observations"],
);
if (!rightNativeResult.ok)
return err(
applicationGraphEvidenceInputError(operation, rightNativeResult.error),
);
try {
const left = parseApplicationGraphEvidence(input.left);
const right = parseApplicationGraphEvidence(input.right);
const leftNative = parseNativeApplicationEvidence(
input.left_native_observations,
);
const rightNative = parseNativeApplicationEvidence(
input.right_native_observations,
);
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,
@@ -128,7 +162,7 @@ export const compareApplicationVersionsEvidenceValidated = (
),
);
} catch (cause: unknown) {
return workflowFailure(operation, cause, input);
return workflowFailure(operation, cause);
}
};
@@ -137,8 +171,15 @@ export const compareSourceToBundleEvidenceValidated = (
input: z.output<typeof compareSourceToBundleInputSchema>,
): Result<Evidence, AnalysisError> => {
const operation = "compare_source_to_bundle";
const applicationResult = parseApplicationGraphEvidence(input.application, [
"application",
]);
if (!applicationResult.ok)
return err(
applicationGraphEvidenceInputError(operation, applicationResult.error),
);
try {
const application = parseApplicationGraphEvidence(input.application);
const application = applicationResult.value;
const result = compareSourceToBundle({
reference: input.reference,
application: {
@@ -157,7 +198,7 @@ export const compareSourceToBundleEvidenceValidated = (
),
);
} catch (cause: unknown) {
return workflowFailure(operation, cause, input);
return workflowFailure(operation, cause);
}
};
@@ -182,9 +223,17 @@ export const compareJavaScriptExportShapesEvidenceValidated = (
input: z.output<typeof compareJavaScriptExportShapesInputSchema>,
): Result<Evidence, AnalysisError> => {
const operation = "compare_javascript_export_shapes";
const leftResult = parseApplicationGraphEvidence(input.left, ["left"]);
if (!leftResult.ok)
return err(applicationGraphEvidenceInputError(operation, leftResult.error));
const rightResult = parseApplicationGraphEvidence(input.right, ["right"]);
if (!rightResult.ok)
return err(
applicationGraphEvidenceInputError(operation, rightResult.error),
);
try {
const left = parseApplicationGraphEvidence(input.left);
const right = parseApplicationGraphEvidence(input.right);
const left = leftResult.value;
const right = rightResult.value;
const result = compareJavaScriptExportShapes({
left: {
evidenceId: left.evidence.evidence_id,
@@ -213,50 +262,17 @@ export const compareJavaScriptExportShapesEvidenceValidated = (
),
);
} catch (cause: unknown) {
return workflowFailure(operation, cause, input);
return workflowFailure(operation, cause);
}
};
const workflowFailure = (
operation: string,
cause: unknown,
input: unknown,
): Result<never, AnalysisError> =>
cause instanceof z.ZodError
? err(analysisInputErrorFromIssues(operation, cause.issues, input))
: cause instanceof TypeError &&
SAFE_APPLICATION_INPUT_CONSTRAINTS.has(cause.message)
? err(
new AnalysisInputError(operation, undefined, [
{ path: [], reason: "invalid_value", message: cause.message },
]),
)
: err(
new AnalysisProtocolError(
"JavaScript application workflow produced an invalid result",
{ cause },
),
);
const SAFE_APPLICATION_INPUT_CONSTRAINTS = new Set([
"Evidence semantic identifier does not match its record",
"Application workflow requires authenticated analyze_javascript_application, reconcile_javascript_runtime, or project_managed_application_graph Evidence",
"JavaScript application Evidence authority or confidence is invalid",
"JavaScript application Evidence predicate does not match its result shape",
"JavaScript application Evidence subject does not match its result",
"Runtime reconciliation Evidence authority or confidence is invalid",
"Runtime reconciliation result references Evidence outside its envelope",
"Runtime reconciliation application layer is missing",
"Managed application graph Evidence authority or confidence is invalid",
"Managed application graph result references Evidence outside its envelope",
"Native handoff Evidence requires an artifact subject",
"Native handoff Evidence must be unique",
...[
"analyze_javascript_application",
"reconcile_javascript_runtime",
"project_managed_application_graph",
].map(
(operation) =>
`Evidence does not match the supported ${operation} contract`,
),
]);
err(
new AnalysisProtocolError(
`JavaScript application workflow failed while producing ${operation}`,
{ cause },
),
);
@@ -85,7 +85,6 @@ const isPortableAbsolutePath = (path: string): boolean =>
/^(?:\\\\|\/\/)[^\\/]+[\\/][^\\/]+(?:[\\/]|$)/u.test(path);
const SAFE_RECONCILIATION_INPUT_CONSTRAINTS = new Set([
"Evidence semantic identifier does not match its record",
"JavaScript application Evidence subject disagrees with its result",
"Runtime reconciliation requires inspect_web_page, inspect_electron_page, observe_javascript_runtime, or capture_electron_scenario Evidence",
"Runtime Evidence target or declared role disagrees with its result",
@@ -5,6 +5,10 @@ import {
AnalysisProtocolError,
} 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";
@@ -13,7 +17,6 @@ import {
traceJavaScriptSemanticsInputSchema,
} from "../../domain/javascript/javascriptSemanticTraceSchemas.js";
import { err, ok, type Result } from "../../domain/result.js";
import { parseApplicationGraphEvidence } from "./JavaScriptApplicationEvidenceGraph.js";
import { createJavaScriptSemanticTraceEvidence } from "./JavaScriptApplicationWorkflowEvidence.js";
const OPERATION = "trace_javascript_semantics" as const;
@@ -22,19 +25,26 @@ const OPERATION = "trace_javascript_semantics" as const;
export const traceJavaScriptSemanticsEvidenceValidated = (
input: z.output<typeof traceJavaScriptSemanticsInputSchema>,
): Result<Evidence, AnalysisError> => {
const sourceResult = parseApplicationGraphEvidence(input.application, [
"application",
]);
if (!sourceResult.ok)
return err(
applicationGraphEvidenceInputError(OPERATION, sourceResult.error),
);
const source = sourceResult.value;
if (source.semanticGraph === null)
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["application"],
reason: "invalid_value",
expected:
"inline Evidence from analyze_javascript_application with a semantic graph",
},
]),
);
try {
const source = parseApplicationGraphEvidence(input.application);
if (source.semanticGraph === null)
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["application"],
reason: "invalid_value",
expected:
"inline Evidence from analyze_javascript_application with a semantic graph",
},
]),
);
const query = queryJavaScriptSemanticGraph(
source.semanticGraph,
input.query,
+13 -7
View File
@@ -140,6 +140,18 @@ export const evidenceSchema = evidenceBaseSchema.superRefine(
validateAnalysisProfileProvider,
);
const authenticatedEvidenceSchema = evidenceSchema.superRefine(
(evidence, context) => {
const { evidence_id: evidenceId, ...withoutId } = evidence;
if (computeEvidenceId(withoutId) !== evidenceId)
context.addIssue({
code: "custom",
path: ["evidence_id"],
message: "Evidence semantic identifier does not match its record",
});
},
);
/** Complete observation whose normalized payload retains its operation-specific type. */
export type Evidence<Result extends JsonValue = JsonValue> = z.infer<
typeof evidenceSchema
@@ -224,13 +236,7 @@ export const parseEvidence = (input: unknown): Evidence => {
? immutableEvidenceSnapshots.get(input)
: undefined;
if (immutable !== undefined) return immutable;
const evidence = evidenceSchema.parse(input);
const { evidence_id: evidenceId, ...withoutId } = evidence;
if (computeEvidenceId(withoutId) !== evidenceId)
throw new TypeError(
"Evidence semantic identifier does not match its record",
);
return evidence;
return authenticatedEvidenceSchema.parse(input);
};
/** Authenticate and seal a ledger-owned snapshot; external mutable values are copied first. */
+14 -1
View File
@@ -243,7 +243,20 @@ export const parseEvidenceBundle = (input: unknown): EvidenceBundle => {
throw new TypeError("Evidence bundle contains duplicate record IDs");
validateUnknownGraph(parsed.unknowns, parsed.records);
const canonical = createEvidenceBundle(
parsed.records.map(parseEvidence),
parsed.records.map((record, index) => {
try {
return parseEvidence(record);
} catch (cause: unknown) {
if (cause instanceof z.ZodError)
throw new z.ZodError(
cause.issues.map((issue) => ({
...issue,
path: ["records", index, ...issue.path],
})),
);
throw cause;
}
}),
parsed.unknowns,
);
if (JSON.stringify(parsed) !== JSON.stringify(canonical))
@@ -313,7 +313,7 @@ describe("evidence bundle filesystem adapter", () => {
error: {
_tag: "EvidenceIntegrityError",
userMessage: expect.stringContaining(
"Evidence semantic identifier does not match its record",
"at records.0.evidence_id: Evidence semantic identifier does not match its record",
),
},
});
@@ -320,7 +320,7 @@ describe("complete comparison projections", () => {
});
describe("JavaScript application workflow error diagnostics", () => {
it("returns safe explicit Evidence constraints from graph workflows", async () => {
it("identifies the inconsistent Evidence subject in graph workflows", async () => {
const root = await createTestTempDirectory(
"rea-application-workflow-diagnostics-",
);
@@ -328,10 +328,10 @@ describe("JavaScript application workflow error diagnostics", () => {
const evidence = await analyzeFixture(root);
const subject = evidence.subject;
if (subject === null) throw new Error("Analysis Evidence subject missing");
const secretPath = "/private/token=do-not-return";
const inconsistentPath = "/different/application.js";
const inconsistent = {
...evidence,
subject: { ...subject, local_path: secretPath },
subject: { ...subject, local_path: inconsistentPath },
};
const result = traceApplicationFeatureEvidence({
@@ -346,11 +346,10 @@ describe("JavaScript application workflow error diagnostics", () => {
expect(result.error.issues).toContainEqual(
expect.objectContaining({
reason: "invalid_value",
message:
"JavaScript application Evidence subject does not match its result",
path: ["application", "subject"],
message: expect.stringContaining("subject must match"),
}),
);
expect(JSON.stringify(result.error.issues)).not.toContain(secretPath);
});
it("preserves schema-authored Evidence validation messages", async () => {
@@ -232,7 +232,9 @@ const analyzeReturn = async (root: string, properties: string) => {
await writeFixtureFiles(root, { "parser.mjs": `${prefix}${expression});\n` });
const analyzed = await analyzeJavaScriptApplication({ input_path: root });
if (!analyzed.ok) throw analyzed.error;
const source = parseApplicationGraphEvidence(analyzed.value);
const parsed = parseApplicationGraphEvidence(analyzed.value);
if (!parsed.ok) throw new Error("Analysis Evidence must parse");
const source = parsed.value;
const observation = source.graph.nodes
.flatMap(({ observations }) => observations)
.find(
@@ -938,7 +938,9 @@ const analyzeGraph = async (root: string) => {
input_path: root,
});
if (!result.ok) throw result.error;
return parseApplicationGraphEvidence(result.value);
const parsed = parseApplicationGraphEvidence(result.value);
if (!parsed.ok) throw new Error("Analysis Evidence must parse");
return parsed.value;
};
const analyzeSources = async (
@@ -15,7 +15,9 @@ const analyze = async (root: string) => {
format: "directory",
});
if (!result.ok) throw result.error;
const authenticated = parseApplicationGraphEvidence(result.value);
const parsed = parseApplicationGraphEvidence(result.value);
if (!parsed.ok) throw new Error("Analysis Evidence must parse");
const authenticated = parsed.value;
return {
graph: authenticated.graph,
result: javascriptApplicationAnalysisResultSchema.parse(
@@ -668,9 +668,16 @@ it("rejects static application Evidence whose subject path disagrees with its re
},
};
expect(() => parseApplicationGraphEvidence(inconsistent)).toThrow(
"JavaScript application Evidence subject does not match its result",
);
const parsed = parseApplicationGraphEvidence(inconsistent);
expect(parsed.ok).toBe(false);
if (!parsed.ok)
expect(parsed.error.issues).toContainEqual(
expect.objectContaining({
path: ["subject"],
reason: "invalid_value",
message: expect.stringContaining("subject must match"),
}),
);
});
const compareTrees = async (previous: string, current: string) => {
@@ -682,9 +689,11 @@ const compareTrees = async (previous: string, current: string) => {
if (!reference.ok) throw new Error(reference.error.message);
const analyzed = await analyzeJavaScriptApplication({ input_path: current });
if (!analyzed.ok) throw analyzed.error;
const application = parseApplicationGraphEvidence(
const parsedApplication = parseApplicationGraphEvidence(
JSON.parse(JSON.stringify(analyzed.value)),
);
if (!parsedApplication.ok) throw new Error("Analysis Evidence must parse");
const application = parsedApplication.value;
const comparison = compareSourceToBundle({
reference: reference.value,
application: {
@@ -0,0 +1,221 @@
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { Ajv2020 } from "ajv/dist/2020.js";
import { createApplicationMcpHarness } from "../fixtures/applicationMcpHarness.js";
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { createCli } from "../../src/cli.js";
import { createEvidence, parseEvidence } from "../../src/domain/evidence.js";
import type { JsonValue } from "../../src/domain/jsonValue.js";
import { analysisCliErrorEnvelopeSchema } from "../../src/contracts/errorSchemas.js";
import { toolContract } from "../../src/contracts/toolContracts.js";
import { javaScriptSemanticTraceResultSchema } from "../../src/domain/javascript/javascriptSemanticTraceSchemas.js";
import { resolveJavaScriptSemanticEvidence } from "../../src/domain/javascript/javascriptSemanticGraph.js";
const SEMANTIC_GRAPH_ID = `jsrg_${"0".repeat(64)}`;
it("reports invalid nested semantic graph Evidence as caller input through CLI and SDK MCP", async () => {
const root = await createTestTempDirectory("rea-semantic-trace-input-");
await mkdir(join(root, "src"));
await writeFile(
join(root, "src", "main.js"),
"export function parse(value) { return JSON.parse(value); }\n",
);
const cli = createCli({});
const analyzed = await serveCli(cli, [
"analyze-javascript-application",
root,
"--json",
]);
expect(analyzed.exitCode).toBe(0);
const sourceEvidence = parseEvidence(JSON.parse(analyzed.stdout));
const normalizedResult = structuredClone(sourceEvidence.normalized_result);
if (!isJsonObject(normalizedResult))
throw new Error("CLI analysis result must be an object");
const semanticGraph = normalizedResult.semantic_graph;
if (!isJsonObject(semanticGraph))
throw new Error("CLI analysis must include a semantic graph");
const firstNode = semanticGraph.nodes;
if (!Array.isArray(firstNode) || !isJsonObject(firstNode[0]))
throw new Error("CLI semantic graph must include a node");
const seedNodeId = firstNode[0].node_id;
if (typeof seedNodeId !== "string")
throw new Error("CLI semantic graph node must have an identifier");
semanticGraph.graph_id = SEMANTIC_GRAPH_ID;
const subject = sourceEvidence.subject;
if (subject === null) throw new Error("CLI Evidence must include a subject");
const malformedEvidence = createEvidence(
{
path: subject.local_path,
sha256: subject.digest.sha256,
format: subject.format,
...(subject.architecture === null
? {}
: { architecture: subject.architecture }),
},
sourceEvidence.provider,
{
predicateType: sourceEvidence.predicate_type,
operation: sourceEvidence.operation,
parameters: sourceEvidence.parameters,
result: normalizedResult,
confidence: sourceEvidence.confidence,
authority: sourceEvidence.authority,
},
);
expect(malformedEvidence.evidence_id).not.toBe(sourceEvidence.evidence_id);
const arguments_ = {
application: malformedEvidence,
query: {
seed: { kind: "semantic-node", node_id: seedNodeId },
direction: "backward-provenance",
},
};
const cliTrace = await serveCli(cli, [
"trace-javascript-semantics",
JSON.stringify(arguments_),
"--json",
]);
const cliEnvelope = analysisCliErrorEnvelopeSchema.parse(
JSON.parse(cliTrace.stdout),
);
const { error: _label, ...cliError } = cliEnvelope;
expect(cliError).toMatchObject({
code: "invalid_request",
category: "invalid_input",
details: {
issues: [
{
path: [
"application",
"normalized_result",
"semantic_graph",
"graph_id",
],
reason: "invalid_value",
},
],
},
});
const harness = await createApplicationMcpHarness();
try {
await assertSuccessfulTraceParity(
cli,
harness,
sourceEvidence,
arguments_.query,
);
const mcpTrace = await harness.client.callTool({
name: "trace_javascript_semantics",
arguments: arguments_,
});
const { error: mcpError } = parseMcpToolError(mcpTrace);
expect(mcpError).toEqual(cliError);
} finally {
await harness.close();
}
});
const assertSuccessfulTraceParity = async (
cli: ReturnType<typeof createCli>,
harness: Awaited<ReturnType<typeof createApplicationMcpHarness>>,
application: ReturnType<typeof parseEvidence>,
query: { readonly seed: unknown; readonly direction: string },
): Promise<void> => {
const arguments_ = { application, query };
const cliResponse = await serveCli(cli, [
"trace-javascript-semantics",
JSON.stringify(arguments_),
"--json",
]);
const cliEvidence = parseEvidence(JSON.parse(cliResponse.stdout));
const cliResult = javaScriptSemanticTraceResultSchema.parse(
cliEvidence.normalized_result,
);
assertTraceContextsResolve(cliResult);
const mcpResponse = await harness.client.callTool({
name: "trace_javascript_semantics",
arguments: arguments_,
});
expect(mcpResponse.isError).not.toBe(true);
const { tools } = await harness.client.listTools();
const advertised = tools.find(
({ name }) => name === "trace_javascript_semantics",
);
if (advertised?.outputSchema === undefined)
throw new Error("Semantic trace SDK output schema is missing");
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const outputSchema: Record<string, unknown> = advertised.outputSchema;
expect(ajv.validateSchema(outputSchema)).toBe(true);
expect(ajv.validate(outputSchema, mcpResponse.structuredContent)).toBe(true);
const mcpEvidence = toolContract(
"trace_javascript_semantics",
).outputSchema.parse(mcpResponse.structuredContent);
expect(mcpEvidence.normalized_result).toEqual(cliResult);
expect(mcpEvidence).toEqual(cliEvidence);
};
const assertTraceContextsResolve = (
result: ReturnType<typeof javaScriptSemanticTraceResultSchema.parse>,
): void => {
const contexts = new Map(
result.evidence_contexts.map((context) => [context.context_id, context]),
);
const references = [
...result.nodes.map(({ evidence }) => evidence),
...result.relations.map(({ evidence }) => evidence),
...result.unknowns.map(({ evidence }) => evidence),
];
const referencedIds = new Set<string>();
for (const reference of references) {
const context = contexts.get(reference.context_id);
expect(context, reference.context_id).toBeDefined();
if (context === undefined) continue;
referencedIds.add(reference.context_id);
expect(
resolveJavaScriptSemanticEvidence(
{ evidence_contexts: result.evidence_contexts },
reference,
),
).toMatchObject({
...Object.fromEntries(
Object.entries(context).filter(([key]) => key !== "context_id"),
),
location: reference.location,
});
}
expect([...referencedIds].toSorted()).toEqual(
[...contexts.keys()].toSorted(),
);
expect(contexts.size).toBeGreaterThan(0);
};
const serveCli = async (
cli: ReturnType<typeof createCli>,
arguments_: readonly string[],
): Promise<{ readonly stdout: string; readonly exitCode: number }> => {
let stdout = "";
let exitCode = 0;
await cli.serve([...arguments_], {
env: {},
exit: (code) => {
exitCode = code;
},
stdout: (text) => {
stdout += text;
},
});
return { stdout, exitCode };
};
const isJsonObject = (value: unknown): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
@@ -175,7 +175,7 @@ it("names the side and constraint when Evidence is not a usable capture", async
error: {
code: "evidence_integrity_mismatch",
message: expect.stringContaining(
"The right Evidence failed validation (Evidence semantic identifier does not match its record)",
"The right Evidence failed validation (at evidence_id: Evidence semantic identifier does not match its record)",
),
},
});