diff --git a/docs/javascript-runtime-reconciliation.md b/docs/javascript-runtime-reconciliation.md index decbd0f844..65b75ea31e 100644 --- a/docs/javascript-runtime-reconciliation.md +++ b/docs/javascript-runtime-reconciliation.md @@ -144,6 +144,10 @@ Script inventory completeness remains a separate reported fact. Unavailable, truncated, or attach-limited worker inventory prevents negative code-presence claims even when the selected page's script inventory is complete. Accepted positive script correspondences remain loaded. +An Electron script's explicit unknown `frame_scope` preserves its reported +frame ID and sourceURL provenance as node properties, but does not link that +script to the current frame inventory. Attach-limited script coverage cannot +establish negative code-presence claims. None of these states proves feature execution, initialization order, reachability, or causality. In particular, `not-observed-in-capture` never means diff --git a/src/browser/execution/CdpRuntimeSources.ts b/src/browser/execution/CdpRuntimeSources.ts index 36ca0d2e7e..d488f3a6bc 100644 --- a/src/browser/execution/CdpRuntimeSources.ts +++ b/src/browser/execution/CdpRuntimeSources.ts @@ -4,9 +4,9 @@ import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import { sanitizeBrowserUrl } from "../../domain/browserObservation.js"; import { jsonObjectSchema } from "../../domain/jsonValue.js"; -import { - type WebRuntimeSource, - type webRuntimeLocationSchema, +import type { + WebRuntimeSource, + webRuntimeLocationSchema, } from "../../domain/webRuntime.js"; import type { CdpEvent } from "../CdpConnection.js"; import type { CdpRuntimeSession } from "./CdpRuntimeSession.js"; diff --git a/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts b/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts index b1b2d441d1..864a7e5c55 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts @@ -506,10 +506,12 @@ const assertRuntimeParameters = ( }; const scriptsComplete = (completeness: { + readonly attach_limited_sections: readonly string[]; readonly truncated_sections: readonly string[]; readonly unavailable_sections: readonly string[]; readonly dropped_events: { readonly scripts: number }; }): boolean => + !completeness.attach_limited_sections.includes("scripts") && !completeness.truncated_sections.includes("scripts") && !completeness.unavailable_sections.includes("scripts") && completeness.dropped_events.scripts === 0; diff --git a/src/domain/javascript/javascriptRuntimeReconciliationRuntime.ts b/src/domain/javascript/javascriptRuntimeReconciliationRuntime.ts index 44b8960c42..7b79b7a8ae 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationRuntime.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationRuntime.ts @@ -157,7 +157,10 @@ const scriptEntities = ( nodeKind: "runtime-script-instance", runtimeKey: script.script_key, targetKey: capture.inspection.target.target_id, - frameKey: script.frame_id, + frameKey: + "frame_scope" in script && script.frame_scope === "unknown" + ? null + : script.frame_id, scriptKey: script.script_key, location, staticKind: "javascript", @@ -176,6 +179,15 @@ const scriptEntities = ( ...("reported_url" in script && script.reported_url !== undefined ? { reported_url: script.reported_url } : {}), + ...("frame_scope" in script && script.frame_scope !== undefined + ? { + frame_scope: script.frame_scope, + reported_frame_id: script.frame_id, + } + : {}), + ...("has_source_url" in script && script.has_source_url !== undefined + ? { has_source_url: script.has_source_url } + : {}), cdp_hash: script.cdp_hash, length: script.length, is_module: script.is_module, diff --git a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts index 05934d6c57..77d51b016b 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts @@ -11,6 +11,7 @@ import { reconcileJavaScriptRuntimeEvidence } from "../../../../src/application/ import { createEvidence } from "../../../../src/domain/evidence.js"; import { mappedRuntimePaths } from "../../../../src/domain/javascript/javascriptRuntimeStaticCandidates.js"; import { projectRuntimeCaptures } from "../../../../src/domain/javascript/javascriptRuntimeReconciliationRuntime.js"; +import { parseRuntimeCaptures } from "../../../../src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.js"; import { electronPageInspectionSchema } from "../../../../src/domain/javascript/electronObservation.js"; import { readRuntimeReconciliationInput, @@ -263,6 +264,63 @@ it("preserves positive script matches without inventing worker absence when work ); }); +it("preserves unknown Electron frame provenance without linking it to an authorized frame", async () => { + const root = await applicationFixture(); + const recorded = electronRuntimeEvidence(root, SOURCE); + const inspection = electronPageInspectionSchema.parse( + recorded.normalized_result, + ); + const evidence = createEvidence(undefined, recorded.provider, { + predicateType: recorded.predicate_type, + operation: recorded.operation, + parameters: recorded.parameters, + confidence: recorded.confidence, + authority: recorded.authority, + result: { + ...inspection, + completeness: { + ...inspection.completeness, + status: "attach_limited", + conditions: ["attach_limited"], + attach_limited_sections: ["scripts"], + }, + scripts: { + ...inspection.scripts, + items: inspection.scripts.items.map((script) => ({ + ...script, + frame_scope: "unknown", + has_source_url: true, + reported_url: "file:///reported-label.js", + })), + }, + }, + }); + const captures = parseRuntimeCaptures([evidence]); + expect(captures[0]?.scriptsCompleteWithinScope).toBe(false); + const projected = projectRuntimeCaptures(captures); + const script = projected.entities.find(({ kind }) => kind === "script"); + expect(script?.frameKey).toBeNull(); + expect(script?.node.properties).toMatchObject({ + frame_scope: "unknown", + reported_frame_id: inspection.scripts.items[0]?.frame_id, + has_source_url: true, + reported_url: "file:///reported-label.js", + }); + const frameIds = new Set( + projected.entities + .filter(({ kind }) => kind === "frame") + .map(({ node }) => node.node_id), + ); + expect( + projected.edges.some( + (edge) => + edge.relation === "contains" && + frameIds.has(edge.source_node_id) && + edge.target_node_id === script?.node.node_id, + ), + ).toBe(false); +}); + it("reconciles active Electron as a partial target-only runtime capture", async () => { const fixture = await applicationFixture(); const staticEvidence = await analyzeFixture(fixture);