diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index 825b36c8b0..210bbff2c1 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -204,6 +204,13 @@ those missing bytes. New inventories preserve an ASAR companion declaration as unavailable bytes belonged to a companion. See [MCP integrity handling](mcp-contracts.md#integrity-record-and-continue) for the tool-result contract. +JavaScript whose syntax cannot be parsed retains unavailable semantic coverage +with `incomplete-module` unknowns for each semantic family and unknown omitted +node and relation extents. The source module is retained when node capacity +permits. A parser failure alone does not imply resource truncation or a named +resource limit. Decoding failures and deliberately +unselected sources retain the same explicit semantic coverage gap. + HTML whose bytes are not valid UTF-8 retains its file observation and an unknown `parse-html-script-references` scope with artifact-byte authority. Reference discovery was unavailable; zero retained script links does not establish that diff --git a/src/application/javascript/JavaScriptArtifactAnalysis.ts b/src/application/javascript/JavaScriptArtifactAnalysis.ts index e79295aaab..7eeb7e3456 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysis.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysis.ts @@ -25,7 +25,10 @@ import type { JavaScriptArtifactFile, JavaScriptArtifactFileSet, } from "../../domain/javascript/javascriptArtifactFiles.js"; -import { javaScriptArtifactTextExclusion } from "../../domain/javascript/javascriptArtifactFiles.js"; +import { + javaScriptArtifactTextExclusion, + javaScriptSemanticSourceExclusion, +} from "../../domain/javascript/javascriptArtifactFiles.js"; import type { AnalyzedJavaScriptArtifactFile, JavaScriptArtifactAnalysisFailure, @@ -189,11 +192,17 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async ( }, }), }); - if (failure !== undefined && facts.projection === null) + const semanticUnavailableReason = + failure?.reason ?? + javaScriptSemanticSourceExclusion( + file, + facts.javascript?.parse_status ?? null, + ); + if (facts.projection === null && semanticUnavailableReason !== null) projection.recordUnavailableFile( file, - failure.reason, - failure.limits, + semanticUnavailableReason, + failure?.limits ?? null, error instanceof AnalysisResourceConstraintError || error instanceof AnalysisTimeoutError ? "resource-limit" diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts index 0eeaa843ec..115f32a75d 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts @@ -20,7 +20,7 @@ import { sourceRangesEqual as rangesEqual } from "../../domain/javascript/javasc import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js"; import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; import { - javaScriptArtifactTextExclusion, + javaScriptSemanticSourceExclusion, type JavaScriptArtifactFile, } from "../../domain/javascript/javascriptArtifactFiles.js"; import { @@ -160,7 +160,10 @@ export const buildJavaScriptSemanticGraph = ({ analyzed.javascript?.electron.call_paths, ); else { - const reason = javaScriptArtifactTextExclusion(analyzed.file); + const reason = javaScriptSemanticSourceExclusion( + analyzed.file, + analyzed.javascript?.parse_status ?? null, + ); if (reason !== null) projection.recordUnavailableFile( analyzed.file, diff --git a/src/application/javascript/JavaScriptSemanticGraphResources.test.ts b/src/application/javascript/JavaScriptSemanticGraphResources.test.ts index 0b4108bc64..8e69279072 100644 --- a/src/application/javascript/JavaScriptSemanticGraphResources.test.ts +++ b/src/application/javascript/JavaScriptSemanticGraphResources.test.ts @@ -6,6 +6,8 @@ import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTyp import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js"; import { resolveJavaScriptSemanticEvidence } from "../../domain/javascript/javascriptSemanticGraph.js"; import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js"; +import { analyzeJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js"; +import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES } from "../../domain/javascript/javascriptSemanticGraphSchemas.js"; const SHA256 = "a".repeat(64); const GRAPH_ID = `jag_${"b".repeat(64)}`; @@ -391,6 +393,56 @@ it("uses unavailable evidence when no source produced semantic IR", () => { ); }); +it("preserves an unparseable source identity and unknown semantic extents", () => { + const source = "export const broken = ;"; + const analysis = analyzeJavaScriptArtifactFiles({ + files: [ + { + path: "broken.mjs", + container_sha256: SHA256, + sha256: SHA256, + bytes: Buffer.byteLength(source), + inventory_artifact_id: `art_${SHA256}`, + kind: "javascript", + unpacked: false, + text: { included: true, value: source }, + }, + ], + unavailable_relevant_files: [], + containers: [], + text_bytes_read: Buffer.byteLength(source), + invalid_utf8_files: 0, + }); + expect(analysis.files[0]?.javascript?.parse_status).toBe("failed"); + const graph = buildJavaScriptSemanticGraph({ + rootArtifactSha256: SHA256, + applicationGraph: { graph_id: GRAPH_ID, nodes: [] }, + analysis, + }); + const module = graph.nodes.find( + ({ kind, identity }) => + kind === "module" && identity.module_path === "broken.mjs", + ); + expect(module).toBeDefined(); + expect(graph.unknowns).toHaveLength( + JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.length, + ); + for (const unknown of graph.unknowns) { + expect(unknown).toMatchObject({ + node_id: module?.node_id, + reason: "incomplete-module", + }); + expect(unknown.detail).toContain("syntax could not be parsed"); + } + expect(graph.coverage).toMatchObject({ + status: "partial", + truncated: false, + omitted_nodes: null, + omitted_relations: null, + limits: [], + }); +}); + const emptyAnalysis = (): JavaScriptArtifactAnalysis => ({ jsonDocuments: new JavaScriptJsonDocuments(), files: [], diff --git a/src/domain/javascript/javascriptArtifactFiles.ts b/src/domain/javascript/javascriptArtifactFiles.ts index 4fcc1ba6bf..477c1c2d0b 100644 --- a/src/domain/javascript/javascriptArtifactFiles.ts +++ b/src/domain/javascript/javascriptArtifactFiles.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { digestSchema } from "../digests.js"; import type { JavaScriptSourceSelection } from "./javascriptSourceSelection.js"; +import type { JavaScriptStaticAnalysis } from "./javascriptStaticAnalysisTypes.js"; import type { ArtifactInventoryCoverage, ArtifactOccurrence, @@ -61,6 +62,20 @@ export const javaScriptArtifactTextExclusion = ( : "Source bytes are not valid UTF-8; JavaScript semantics were not analyzed."; }; +/** Source availability shared by semantic consumers of parsed artifact facts. */ +export const javaScriptSemanticSourceExclusion = ( + file: JavaScriptArtifactFile, + parseStatus: JavaScriptStaticAnalysis["parse_status"] | null, +): string | null => { + if (file.kind !== "javascript") return null; + return ( + javaScriptArtifactTextExclusion(file) ?? + (parseStatus === "failed" + ? "JavaScript syntax could not be parsed; JavaScript semantics were not analyzed." + : null) + ); +}; + /** * One inventoried regular file not backed by analyzed source bytes. Its inventory * occurrence remains available even when content is opaque or unavailable. diff --git a/tests/acceptance/applications/javascriptSourceSelectionCli.test.ts b/tests/acceptance/applications/javascriptSourceSelectionCli.test.ts index 212bfa0a1f..1e26196c37 100644 --- a/tests/acceptance/applications/javascriptSourceSelectionCli.test.ts +++ b/tests/acceptance/applications/javascriptSourceSelectionCli.test.ts @@ -115,6 +115,19 @@ cliTest.for([ detail.includes("not valid UTF-8"), ), ).toHaveLength(JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.length); + const unparsedModule = full.semantic_graph.nodes.find( + ({ kind, identity }) => + kind === "module" && identity.module_path === "vendor.js", + ); + expect(unparsedModule).toBeDefined(); + expect( + full.semantic_graph.unknowns.filter( + ({ node_id, detail, reason }) => + node_id === unparsedModule?.node_id && + reason === "incomplete-module" && + detail.includes("syntax could not be parsed"), + ), + ).toHaveLength(JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.length); expect(full.semantic_graph.coverage).toMatchObject({ status: "partial", truncated: false,