From 1e421242b97a533a5c03458ac074d4353ec17bf3 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:53:03 +0800 Subject: [PATCH] fix(javascript): preserve uncertainty for incomplete runtime captures (#7854) --- .../javascript/javascriptRuntimeLoadState.ts | 22 +++++++--- .../javascriptRuntimeReconciliation.ts | 1 + .../javascriptRuntimeContentPriority.test.ts | 1 + .../javascriptRuntimeReconciliation.test.ts | 43 +++++++++++++++++++ 4 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/domain/javascript/javascriptRuntimeLoadState.ts b/src/domain/javascript/javascriptRuntimeLoadState.ts index 49703719d3..275de19d37 100644 --- a/src/domain/javascript/javascriptRuntimeLoadState.ts +++ b/src/domain/javascript/javascriptRuntimeLoadState.ts @@ -1,6 +1,7 @@ import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js"; import { uniqueSorted } from "../canonicalOrdering.js"; import type { ParsedStaticLayer } from "./javascriptRuntimeReconciliationParsing.js"; +import type { ParsedRuntimeCapture } from "./javascriptRuntimeReconciliationCaptureParsing.js"; import type { JavaScriptRuntimeReconciliationItem, JavaScriptStaticLoadState, @@ -20,6 +21,7 @@ export interface StaticLoadStateProjection { /** Classify static code presence without claiming bundled module execution. */ export const classifyStaticLoadStates = ( layers: readonly ParsedStaticLayer[], + captures: readonly ParsedRuntimeCapture[], runtimeEntities: readonly RuntimeReconciliationEntity[], matches: readonly JavaScriptRuntimeReconciliationItem[], options: { @@ -28,9 +30,15 @@ export const classifyStaticLoadStates = ( ): StaticLoadStateProjection => { const states: JavaScriptStaticLoadState[] = []; for (const layer of layers) { - const projection = classifyLayer(layer, runtimeEntities, matches, { - reconciliationComplete: options.reconciliationComplete, - }); + const projection = classifyLayer( + layer, + captures, + runtimeEntities, + matches, + { + reconciliationComplete: options.reconciliationComplete, + }, + ); states.push(...projection.states); } return { states, omittedStates: 0 }; @@ -38,6 +46,7 @@ export const classifyStaticLoadStates = ( const classifyLayer = ( layer: ParsedStaticLayer, + captures: readonly ParsedRuntimeCapture[], runtimeEntities: readonly RuntimeReconciliationEntity[], matches: readonly JavaScriptRuntimeReconciliationItem[], options: { @@ -70,8 +79,11 @@ const classifyLayer = ( options.reconciliationComplete && layer.graph.coverage.status === "complete" && scopedEntities.length > 0 && - scopedEntities.every( - ({ capture }) => capture.codeInventoryCompleteWithinScope, + // An incomplete inventory may have omitted scripts that map into this + // layer, even when none of its reported entities map here. + captures.every( + ({ codeInventoryCompleteWithinScope }) => + codeInventoryCompleteWithinScope, ); const states = nodes.map((node) => { const runtimeNodeIds = uniqueSorted(direct.get(node.node_id) ?? []); diff --git a/src/domain/javascript/javascriptRuntimeReconciliation.ts b/src/domain/javascript/javascriptRuntimeReconciliation.ts index cb9e1a3b8e..443be04bb7 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliation.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliation.ts @@ -58,6 +58,7 @@ export const reconcileJavaScriptRuntime = ( }); const loadStates = classifyStaticLoadStates( layers, + captures, runtime.entities, matching.items, { diff --git a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeContentPriority.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeContentPriority.test.ts index af9e5a50e8..058a0abe65 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeContentPriority.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeContentPriority.test.ts @@ -220,6 +220,7 @@ it("keeps unavailable mapped digests unknown instead of borrowing unrelated matc const candidate = script?.candidate_static_nodes[0]; const states = classifyStaticLoadStates( parsed.layers, + parsed.captures, runtime.entities, result.items, { diff --git a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts index a1cf7ec974..3e42a52898 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts @@ -244,6 +244,49 @@ it("keeps static load states unknown when one scoped capture omits scripts", asy ); }); +it("does not infer absence from an incomplete capture with no mapped entities", async () => { + const fixture = await applicationFixture(); + const outside = await applicationFixture(); + const analysis = await analyzeFixture(fixture); + const complete = electronRuntimeEvidence(fixture, SOURCE, { + includeWorker: false, + targetId: "complete", + }); + const outsideCapture = (scriptsUnavailable: boolean) => + electronRuntimeEvidence(outside, "export const unrelated = 1;", { + includeWorker: false, + scriptsUnavailable, + targetId: "outside", + }); + const reconcile = ( + runtime_observations: ReturnType[], + ) => + reconcileInput({ + static_layers: [{ role: "application", analysis }], + runtime_observations, + }); + + const control = reconcile([complete, outsideCapture(false)]); + expect(control.summary.static_not_observed).toBeGreaterThan(0); + const incomplete = outsideCapture(true); + for (const captures of [ + [complete, incomplete], + [incomplete, complete], + ]) { + const result = reconcile(captures); + expect(result.summary.static_not_observed).toBe(0); + expect(result.static_load_states).toEqual( + expect.arrayContaining([ + expect.objectContaining({ status: "loaded" }), + expect.objectContaining({ + status: "unknown", + reason: "static-or-runtime-coverage-incomplete", + }), + ]), + ); + } +}); + it("preserves positive script matches without inventing worker absence when workers are unavailable", async () => { const fixture = await applicationFixture(); const staticEvidence = await analyzeFixture(fixture);