diff --git a/src/application/javascript/JavaScriptApplicationService.test.ts b/src/application/javascript/JavaScriptApplicationService.test.ts index c76796c02..8529ff333 100644 --- a/src/application/javascript/JavaScriptApplicationService.test.ts +++ b/src/application/javascript/JavaScriptApplicationService.test.ts @@ -8,6 +8,7 @@ import { z } from "zod"; import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js"; +import { compareApplicationVersionsEvidence } from "./JavaScriptApplicationWorkflowService.js"; describe("JavaScript application failure diagnostics", () => { it("identifies the rejected result field in caller-visible diagnostics", async () => { @@ -122,6 +123,9 @@ describe("JavaScript application failure diagnostics", () => { ); }); +const CONTRADICTION_LIMITATION = + "Observed bytes of 1 artifact file(s) contradict declared integrity and are untrusted; contradicted nested archives were not expanded: addon.node."; + describe("JavaScript application artifact integrity", () => { it("analyzes a signed-after-packaging native module only when the mismatch is recorded", async () => { const root = await createTestTempDirectory("rea-js-integrity-"); @@ -169,9 +173,27 @@ describe("JavaScript application artifact integrity", () => { trust: "observed-untrusted", }, ], - limitations: expect.arrayContaining([ - "1 artifact file(s) contradict declared integrity; their observed bytes are untrusted, and contradicted nested archives were not expanded.", - ]), + graph: { coverage: { status: "partial" } }, + limitations: expect.arrayContaining([CONTRADICTION_LIMITATION]), + semantic_graph: { + limitations: expect.arrayContaining([CONTRADICTION_LIMITATION]), + }, + }); + + // Workflows built on the recorded graph inherit its partial coverage. + const clean = await analyzeJavaScriptApplication({ + input_path: source, + format: "directory", + }); + if (!clean.ok) throw new Error("Expected clean analysis"); + const comparison = compareApplicationVersionsEvidence({ + left: clean.value, + right: recorded.value, + }); + expect(comparison.ok).toBe(true); + if (!comparison.ok) throw new Error("Expected comparison"); + expect(comparison.value.normalized_result).toMatchObject({ + coverage: { status: "partial", right_graph_status: "partial" }, }); }); }); diff --git a/src/application/javascript/JavaScriptApplicationService.ts b/src/application/javascript/JavaScriptApplicationService.ts index 8ff28e816..fd845f798 100644 --- a/src/application/javascript/JavaScriptApplicationService.ts +++ b/src/application/javascript/JavaScriptApplicationService.ts @@ -60,18 +60,10 @@ export const analyzeJavaScriptApplicationValidated = async ( options.signal, ); const { electron_summary: summary, ...application } = reconstructed; - const contradictions = reconstructed.integrity_contradictions.length; const result = javascriptApplicationAnalysisResultSchema.parse({ ...application, summary, - limitations: [ - ...reconstructed.graph.limitations, - ...(contradictions === 0 - ? [] - : [ - `${String(contradictions)} artifact file(s) contradict declared integrity; their observed bytes are untrusted, and contradicted nested archives were not expanded.`, - ]), - ], + limitations: reconstructed.graph.limitations, }); await options.progress?.report({ phase: "analyze_javascript_application", diff --git a/src/application/javascript/JavaScriptArtifactGraphBuilder.ts b/src/application/javascript/JavaScriptArtifactGraphBuilder.ts index 8c187e581..892454aef 100644 --- a/src/application/javascript/JavaScriptArtifactGraphBuilder.ts +++ b/src/application/javascript/JavaScriptArtifactGraphBuilder.ts @@ -92,6 +92,7 @@ const graphCoverage = (context: JavaScriptArtifactGraphContext) => { javascript !== null && javascript.parse_status === "partial", ); const unknownGap = + context.snapshot.integrity_contradictions.length > 0 || context.analysis.parse_failures > 0 || context.fileSet.invalid_utf8_files > 0 || sourceMapPolicyGap || @@ -103,6 +104,20 @@ const graphCoverage = (context: JavaScriptArtifactGraphContext) => { return completeApplicationCoverage(); }; +/** + * Disclose recorded integrity contradictions wherever the application graph + * is consumed; the graph is also partial, so comparisons and traces built on + * it do not claim completeness. + */ +export const integrityContradictionLimitations = ( + snapshot: Pick, +): string[] => + snapshot.integrity_contradictions.length === 0 + ? [] + : [ + `Observed bytes of ${String(snapshot.integrity_contradictions.length)} artifact file(s) contradict declared integrity and are untrusted; contradicted nested archives were not expanded: ${snapshot.integrity_contradictions.map(({ logical_path: path }) => path).join(", ")}.`, + ]; + const graphLimitations = ( context: JavaScriptArtifactGraphContext, coverage: "complete" | "partial" | "unknown" | "unavailable", @@ -124,6 +139,7 @@ const graphLimitations = ( ); return [ ...context.analysis.limitations, + ...integrityContradictionLimitations(context.snapshot), "CommonJS and ESM binding relationships were recovered from inert syntax and resolved only within the inventoried artifact container.", "Webpack/Rspack factories were recovered from AST literals; REA did not invoke push handlers or bundle bootstrap code.", "Static imports, entrypoints, workers, endpoints, and storage relationships do not prove runtime execution.", diff --git a/src/application/javascript/JavaScriptArtifactReconstruction.ts b/src/application/javascript/JavaScriptArtifactReconstruction.ts index 84dfa7628..64dc153c4 100644 --- a/src/application/javascript/JavaScriptArtifactReconstruction.ts +++ b/src/application/javascript/JavaScriptArtifactReconstruction.ts @@ -12,7 +12,10 @@ import type { ElectronBoundarySummary } from "../../domain/javascriptApplication import type { IntegrityContradiction } from "../../domain/artifactGraph.js"; import { analyzeJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js"; import { readJavaScriptArtifactFiles } from "./JavaScriptArtifactFiles.js"; -import { buildJavaScriptArtifactGraph } from "./JavaScriptArtifactGraphBuilder.js"; +import { + buildJavaScriptArtifactGraph, + integrityContradictionLimitations, +} from "./JavaScriptArtifactGraphBuilder.js"; import { javascriptArtifactReconstructionInputSchema, type JavaScriptArtifactReconstructionInput, @@ -75,6 +78,7 @@ export const reconstructJavaScriptArtifact = async ( rootArtifactSha256: snapshot.manifest.root_sha256, applicationGraph: graph, analysis, + sourceLimitations: integrityContradictionLimitations(snapshot), }); return { input_path: path, diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts index 7f36a77d1..827801918 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts @@ -58,6 +58,8 @@ interface BuilderInput { "graph_id" | "nodes" >; readonly analysis: JavaScriptArtifactAnalysis; + /** Source facts the semantic graph inherits, such as integrity contradictions. */ + readonly sourceLimitations?: readonly string[]; } interface FileContext extends SemanticFlowProjectionContext { @@ -79,6 +81,7 @@ export const buildJavaScriptSemanticGraph = ({ rootArtifactSha256, applicationGraph, analysis, + sourceLimitations = [], }: BuilderInput): JavaScriptSemanticGraph => { const state = emptyState(applicationGraph); const fingerprints: JavaScriptSemanticGraph["fingerprints"][number][] = []; @@ -119,6 +122,7 @@ export const buildJavaScriptSemanticGraph = ({ })), }, limitations: [ + ...sourceLimitations, "The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.", "Local data flow does not claim control-flow-sensitive reaching definitions or arbitrary dynamic property resolution.", "Promise ownership covers explicit unshadowed Promise construction, static factories, aggregation, chaining, and await syntax only.", diff --git a/src/domain/analysisErrorPresentation.ts b/src/domain/analysisErrorPresentation.ts index 41233e32c..0eaa65a1a 100644 --- a/src/domain/analysisErrorPresentation.ts +++ b/src/domain/analysisErrorPresentation.ts @@ -130,7 +130,7 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => { return error.reason === "integrity" && error.artifactDetails !== undefined && INTEGRITY_POLICY_OPERATIONS.has(error.operation) - ? "Artifact bytes contradict their declared integrity. If that is expected, such as an unpacked native module signed after packaging, rerun with integrity_policy record-and-continue to keep the mismatch as an untrusted contradiction; otherwise get a fresh copy." + ? "Artifact bytes contradict their declared integrity. If that is expected, such as an unpacked native module signed after packaging, rerun with the record-and-continue integrity policy (integrity_policy in MCP, --integrity-policy on the CLI) to keep the mismatch as an untrusted contradiction; otherwise get a fresh copy." : artifactMessage(error.reason); if (error instanceof EvidenceReferenceError) return error.reason === "missing" diff --git a/tests/boundary/mcp/artifactMcp.test.ts b/tests/boundary/mcp/artifactMcp.test.ts index 5ae6eec08..d44135a83 100644 --- a/tests/boundary/mcp/artifactMcp.test.ts +++ b/tests/boundary/mcp/artifactMcp.test.ts @@ -91,7 +91,7 @@ it.each([ ); const INTEGRITY_REMEDIATION = - "Artifact bytes contradict their declared integrity. If that is expected, such as an unpacked native module signed after packaging, rerun with integrity_policy record-and-continue to keep the mismatch as an untrusted contradiction; otherwise get a fresh copy."; + "Artifact bytes contradict their declared integrity. If that is expected, such as an unpacked native module signed after packaging, rerun with the record-and-continue integrity policy (integrity_policy in MCP, --integrity-policy on the CLI) to keep the mismatch as an untrusted contradiction; otherwise get a fresh copy."; const artifactInventoryResultSchema = z.object({ occurrences: z.array(