From 5aafaa00ecd76065cc40c7adf4d66ae8d9d3c764 Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Wed, 7 Oct 2026 08:50:41 +0000 Subject: [PATCH] fix(artifacts): carry recorded integrity contradictions into application workflows A recorded contradiction appeared only in the analyze_javascript_application response. Version and source comparisons, feature traces, and export-shape comparisons read the application graph, which still claimed complete coverage, so they could report complete-within-inputs results over untrusted bytes. The graph is now partial when contradictions were recorded, and its limitations, inherited by the result and the semantic graph, name every contradicted path. The mismatch remediation names both spellings: integrity_policy in MCP and --integrity-policy on the CLI. Co-Authored-By: Claude Opus 5.5 --- .../JavaScriptApplicationService.test.ts | 28 +++++++++++++++++-- .../JavaScriptApplicationService.ts | 10 +------ .../JavaScriptArtifactGraphBuilder.ts | 16 +++++++++++ .../JavaScriptArtifactReconstruction.ts | 6 +++- .../JavaScriptSemanticGraphBuilder.ts | 4 +++ src/domain/analysisErrorPresentation.ts | 2 +- tests/boundary/mcp/artifactMcp.test.ts | 2 +- 7 files changed, 53 insertions(+), 15 deletions(-) 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(