From 1cc16128ea1a9bbf3470859a93a868661051bec5 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 04:11:22 +0800 Subject: [PATCH] fix(javascript): retain unavailable source processing scopes --- docs/javascript-artifact-reconstruction.md | 3 ++ .../JavaScriptArtifactGraphContext.ts | 15 ++++++-- .../JavaScriptArtifactGraphStructure.ts | 34 ++++++++++++++--- .../javascriptDirectoryCoverage.test.ts | 38 +++++++++++++++++++ 4 files changed, 80 insertions(+), 10 deletions(-) diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index bd933421a8..c4d53f1039 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -118,6 +118,9 @@ descendant omission. Readable sources remain available; application coverage is partial and retains those inventory limitations. An unreadable selected root remains an error. Explicit readers that require selected child bytes keep their access errors rather than treating denied paths as absent. +Unavailable source files also retain their applicable per-file processing gap +in the application graph. Their container inventory identity anchors that gap; +REA does not invent a content digest for bytes it could not read. Incomplete traversal also qualifies negative path and package-metadata lookups: an unobserved earlier loader candidate cannot be skipped in favor of a later observed candidate. An explicit source selection whose discovery is unknown diff --git a/src/application/javascript/JavaScriptArtifactGraphContext.ts b/src/application/javascript/JavaScriptArtifactGraphContext.ts index e89b217bf8..5a5365f3fc 100644 --- a/src/application/javascript/JavaScriptArtifactGraphContext.ts +++ b/src/application/javascript/JavaScriptArtifactGraphContext.ts @@ -216,7 +216,10 @@ export interface RoleAssetLinkInput { /** An explicit processing/selection gap with the applicable phase's authority. */ export interface UnavailableParseInput { - readonly file: JavaScriptArtifactFile; + /** Missing source bytes are anchored to their observed inventory container. */ + readonly file: Pick & { + readonly sha256: string | null; + }; readonly asset: ApplicationNode; readonly operation: string; readonly authority: JavaScriptArtifactAnalysisScope["authority"]; @@ -319,10 +322,11 @@ export const addUnavailableStaticParseScope = ( context: JavaScriptArtifactGraphContext, input: UnavailableParseInput, ): void => { + const artifactSha256 = input.file.sha256 ?? input.file.container_sha256; const node = context.accumulator.addNode({ kind: "unknown", identity: artifactLocalIdentity( - input.file.sha256, + artifactSha256, "static-parse-scope", `${input.operation}:${input.file.path}`, ), @@ -335,13 +339,16 @@ export const addUnavailableStaticParseScope = ( path: input.file.path, operation: input.operation, limitation: input.limitation, + ...(input.file.sha256 === null + ? { source_bytes_available: false } + : {}), ...(input.failure === undefined ? {} : { analysis_failure: input.failure }), }, evidence: unavailableStaticEvidence({ authority: input.authority, - sha256: input.file.sha256, + sha256: artifactSha256, path: input.file.path, operation: input.operation, coverage: @@ -362,7 +369,7 @@ export const addUnavailableStaticParseScope = ( relation: "contains", properties: { scope: "unavailable-static-analysis" }, evidence: artifactObservationEvidence({ - sha256: input.file.sha256, + sha256: artifactSha256, path: input.file.path, operation: "associate-unavailable-parse-scope", coverage: completeApplicationCoverage(), diff --git a/src/application/javascript/JavaScriptArtifactGraphStructure.ts b/src/application/javascript/JavaScriptArtifactGraphStructure.ts index 720e4ea419..55e145bb53 100644 --- a/src/application/javascript/JavaScriptArtifactGraphStructure.ts +++ b/src/application/javascript/JavaScriptArtifactGraphStructure.ts @@ -356,20 +356,23 @@ export const addJavaScriptArtifactFiles = ( }, }); } - addUnavailableAsarEntries(context); + addUnavailableArtifactEntries(context); }; -const addUnavailableAsarEntries = ( +const addUnavailableArtifactEntries = ( context: JavaScriptArtifactGraphContext, ): void => { for (const file of context.fileSet.unavailable_relevant_files) { - if (!isAsarContainer(context, file.container_sha256)) continue; + const asar = isAsarContainer(context, file.container_sha256); + const operation = asar + ? "inventory-asar-unavailable-entry" + : "inventory-unavailable-entry"; const limitations = [ ...(file.limitations ?? []), `The inventoried ${file.kind ?? "relevant"} entry has unavailable bytes; no content artifact identity can be established.`, ]; const entry = context.accumulator.addNode({ - kind: "asar-entry", + kind: asar ? "asar-entry" : "artifact", identity: { strategy: "canonical-path", stability: "artifact-version", @@ -380,6 +383,7 @@ const addUnavailableAsarEntries = ( { label: file.path, properties: { + path: file.path, entry_sha256: file.sha256, bytes: file.bytes, inventory_artifact_id: file.inventory_artifact_id, @@ -394,7 +398,7 @@ const addUnavailableAsarEntries = ( evidence: artifactObservationEvidence({ sha256: file.container_sha256, path: file.path, - operation: "inventory-asar-unavailable-entry", + operation, coverage: partialApplicationCoverage([], null), limitations, }), @@ -415,11 +419,29 @@ const addUnavailableAsarEntries = ( evidence: artifactObservationEvidence({ sha256: file.container_sha256, path: file.path, - operation: "inventory-asar-unavailable-entry", + operation, coverage: partialApplicationCoverage([], null), limitations, }), }); + const scope = + file.kind === undefined + ? null + : javascriptArtifactAnalysisScopes[file.kind]; + if (scope !== null) + addUnavailableStaticParseScope(context, { + file, + asset: entry, + ...scope, + limitation: limitations.join(" "), + ...(file.inventory_occurrence_id === undefined + ? {} + : { + details: { + inventory_occurrence_id: file.inventory_occurrence_id, + }, + }), + }); if (file.kind === "native-addon" || file.artifact_kind === "native-addon") { const addon = context.accumulator.addNode({ kind: "native-addon", diff --git a/tests/boundary/filesystem/javascriptDirectoryCoverage.test.ts b/tests/boundary/filesystem/javascriptDirectoryCoverage.test.ts index d47ea4f6d7..97196faac5 100644 --- a/tests/boundary/filesystem/javascriptDirectoryCoverage.test.ts +++ b/tests/boundary/filesystem/javascriptDirectoryCoverage.test.ts @@ -102,6 +102,44 @@ it.skipIf(process.platform === "win32" || process.getuid?.() === 0)( expect(result.graph.limitations).toEqual( expect.arrayContaining([...inventory.limitations]), ); + const unavailableScopes = result.graph.nodes + .filter(({ kind }) => kind === "unknown") + .flatMap(({ observations }) => observations) + .filter( + ({ properties }) => + properties.path === "closed.js" && + properties.operation === "analyze-javascript-source", + ); + expect(unavailableScopes).toHaveLength(1); + expect(unavailableScopes[0]).toMatchObject({ + properties: { + source_bytes_available: false, + inventory_occurrence_id: inventory.occurrences.find( + ({ logical_path }) => logical_path === "closed.js", + )?.occurrence_id, + }, + evidence: { + authority: "ast-static-analysis", + state: "unavailable", + artifact: { available: true, sha256: result.root_artifact_sha256 }, + }, + }); + const unavailableEntry = result.graph.nodes.find( + ({ identity }) => + identity.strategy === "canonical-path" && + identity.path === "closed.js", + ); + expect(unavailableEntry).toMatchObject({ + kind: "artifact", + observations: [ + expect.objectContaining({ + properties: expect.objectContaining({ content_available: false }), + evidence: expect.objectContaining({ + operation: "inventory-unavailable-entry", + }), + }), + ], + }); expect( result.semantic_graph.nodes.some( ({ identity }) => identity.module_path === "good.js",