From e308f35c43bb2efaef56b17ef78bfa2bf2d6f090 Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:20:21 +0000 Subject: [PATCH] fix(javascript): keep unmapped captured runtime scripts unknown A captured script whose bytes matched no static digest was reported as high-confidence unmatched even when no automatic or caller-declared mapping applied to its location in any static layer. The matcher now computes mapping presence once while locating candidates and shares one no-candidate classification between the captured-source and source-free branches, so an unmapped location stays unknown with no-authorized-location-mapping and is not counted as unmatched. Fixes #6061. Co-Authored-By: Claude Opus 5.5 --- docs/javascript-runtime-reconciliation.md | 6 +- ...javascriptRuntimeReconciliationMatching.ts | 94 +++++--- .../javascriptRuntimeContentPriority.test.ts | 201 ++++++++++++++++++ 3 files changed, 266 insertions(+), 35 deletions(-) diff --git a/docs/javascript-runtime-reconciliation.md b/docs/javascript-runtime-reconciliation.md index 7efbfc3e2d..a274deba12 100644 --- a/docs/javascript-runtime-reconciliation.md +++ b/docs/javascript-runtime-reconciliation.md @@ -106,7 +106,11 @@ nodes. REA evaluates compatible static candidates conservatively: that disagreement. If a mapped candidate's digest is unavailable and no mapped content match is known, the comparison remains `unknown`. 6. Missing authorization mappings and incomplete or truncated input coverage - remain unknown or truncated rather than becoming absence claims. + remain unknown or truncated rather than becoming absence claims. A runtime + location with no automatic artifact-root or caller-declared mapping into + any layer, and no unique content match, is `unknown` with + `no-authorized-location-mapping`, with or without captured bytes. Only a + mapped location can be `unmatched` with `no-static-content-match`. Every successful cross-layer `observed_as` edge has `authority: cross-layer-reconciliation` and `state: inferred`. The underlying diff --git a/src/domain/javascript/javascriptRuntimeReconciliationMatching.ts b/src/domain/javascript/javascriptRuntimeReconciliationMatching.ts index 5e6ca4cd1e..4321517d42 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationMatching.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationMatching.ts @@ -77,14 +77,13 @@ const reconcileEntity = ( reason: "runtime-location-unresolved", candidates: [], }); - const locations = candidatesWithLocation(entity, index, layers); + const { locations, mapped } = locateEntity(entity, index, layers); if (entity.sourceSha256 !== null) - return reconcileCapturedSource( - entity, - entity.sourceSha256, + return reconcileCapturedSource(entity, entity.sourceSha256, { locations, + mapped, index, - ); + }); const location = locations.at(0); if (locations.length === 1 && location !== undefined) { @@ -103,20 +102,40 @@ const reconcileEntity = ( reason: "ambiguous-static-candidates", candidates: locations.map(({ candidate }) => candidate), }); - const mapped = layers.some( - (layer) => mappedRuntimePaths(layer, entity).length > 0, - ); - return unresolved(entity, { - status: mapped ? "unmatched" : "unknown", - basis: "none", - confidence: "unknown", - reason: mapped - ? "no-static-content-match" - : "no-authorized-location-mapping", - candidates: [], - }); + return withoutStaticCandidate(entity, mapped, "unknown"); }; +/** + * Classify an entity with no qualifying static candidate. Absence is claimed + * only beneath an automatic or caller-declared mapping; without one, the + * runtime location has no authorized static counterpart and stays unknown. + * Captured bytes compared against every static digest support a high-confidence + * absence; a location-only absence does not. + */ +const withoutStaticCandidate = ( + entity: RuntimeReconciliationEntity, + mapped: boolean, + absenceConfidence: JavaScriptRuntimeReconciliationItem["confidence"], +): EvaluatedMatch => + unresolved( + entity, + mapped + ? { + status: "unmatched", + basis: "none", + confidence: absenceConfidence, + reason: "no-static-content-match", + candidates: [], + } + : { + status: "unknown", + basis: "none", + confidence: "unknown", + reason: "no-authorized-location-mapping", + candidates: [], + }, + ); + interface LocatedCandidate { readonly candidate: StaticRuntimeCandidate; readonly mapping: MappedRuntimePath; @@ -125,8 +144,15 @@ interface LocatedCandidate { const reconcileCapturedSource = ( entity: RuntimeReconciliationEntity, sourceSha256: string, - locations: readonly LocatedCandidate[], - index: CandidateIndex, + { + locations, + mapped, + index, + }: { + readonly locations: readonly LocatedCandidate[]; + readonly mapped: boolean; + readonly index: CandidateIndex; + }, ): EvaluatedMatch => { // A known mapped candidate owns the comparison. Unrelated equal bytes cannot // repair a disagreement or establish the content of a candidate without a digest. @@ -176,23 +202,19 @@ const reconcileCapturedSource = ( candidates: locations.map(({ candidate: located }) => located), }); } - return unresolved(entity, { - status: "unmatched", - basis: "none", - confidence: "high", - reason: "no-static-content-match", - candidates: [], - }); + return withoutStaticCandidate(entity, mapped, "high"); }; -const candidatesWithLocation = ( +const locateEntity = ( entity: RuntimeReconciliationEntity, index: CandidateIndex, layers: readonly ParsedStaticLayer[], -): LocatedCandidate[] => { +): { readonly locations: LocatedCandidate[]; readonly mapped: boolean } => { const located = new Map(); + let mapped = false; for (const layer of layers) - for (const mapping of mappedRuntimePaths(layer, entity)) + for (const mapping of mappedRuntimePaths(layer, entity)) { + mapped = true; for (const candidate of [ ...(index.byLayerCategoryPath.get( layerCategoryPathKey(layer.layerId, entity.staticKind, mapping.path), @@ -215,12 +237,16 @@ const candidatesWithLocation = ( const key = candidateKey(candidate); if (!located.has(key)) located.set(key, { candidate, mapping }); } - return [...located.values()].sort((left, right) => - compareUnicodeCodePoints( - candidateKey(left.candidate), - candidateKey(right.candidate), + } + return { + locations: [...located.values()].sort((left, right) => + compareUnicodeCodePoints( + candidateKey(left.candidate), + candidateKey(right.candidate), + ), ), - ); + mapped, + }; }; const indexCandidates = ( diff --git a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeContentPriority.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeContentPriority.test.ts index 7e46294aa3..867df5b05c 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeContentPriority.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeContentPriority.test.ts @@ -229,3 +229,204 @@ it("keeps unavailable mapped digests unknown instead of borrowing unrelated matc reason: "static-or-runtime-coverage-incomplete", }); }); + +const UNRELATED = "export const unrelated = 17;\n"; + +type LayerSetup = { + readonly root: string; + readonly mapping: "none" | "explicit"; +}; + +/** Reconcile one captured script, located at absent.js under live, against layers. */ +const reconcileCapturedScript = async (input: { + readonly layers: readonly LayerSetup[]; + readonly live: string; + readonly source: string; + readonly includeWorker?: boolean; +}) => { + const result = reconcileJavaScriptRuntime( + parseRuntimeReconciliationInput( + reconcileJavaScriptRuntimeInputSchema.parse({ + static_layers: await Promise.all( + input.layers.map(async ({ root, mapping }, index) => ({ + role: (["application", "cache", "assets"] as const)[index], + analysis: await analyzeFixture(root), + runtime_mappings: + mapping === "explicit" + ? [{ kind: "file-root", root: input.live, artifact_prefix: "" }] + : [], + })), + ), + runtime_observations: [ + electronRuntimeEvidence(input.live, input.source, { + scriptFile: "absent.js", + includeWorker: input.includeWorker ?? false, + }), + ], + }), + ), + ); + const item = (kind: string) => + result.reconciliations.find(({ entity_kind }) => entity_kind === kind); + return { result, script: item("script"), worker: item("worker") }; +}; + +it("keeps an unmapped captured script without a content match unknown", async () => { + const root = await applicationFixture(); + const live = await createTestTempDirectory("rea-runtime-unmapped-"); + const { result, script, worker } = await reconcileCapturedScript({ + layers: [{ root, mapping: "none" }], + live, + source: UNRELATED, + includeWorker: true, + }); + const unknown = { + status: "unknown", + basis: "none", + confidence: "unknown", + reason: "no-authorized-location-mapping", + static_layer_id: null, + static_node_id: null, + candidate_static_count: 0, + }; + expect(script).toMatchObject(unknown); + // A source-free worker at an unmapped location shares the same rule. + expect(worker).toMatchObject(unknown); + expect(result.summary).toMatchObject({ matched: 0, unmatched: 0 }); + expect( + result.graph.edges.some(({ relation }) => relation === "observed_as"), + ).toBe(false); +}); + +it.each(["explicit", "implicit-root"] as const)( + "reports a mapped captured script without a content match as unmatched (%s)", + async (mapping) => { + const root = await applicationFixture(); + const live = + mapping === "explicit" + ? await createTestTempDirectory("rea-runtime-mapped-absent-") + : root; + const { result, script } = await reconcileCapturedScript({ + layers: [{ root, mapping: mapping === "explicit" ? "explicit" : "none" }], + live, + source: UNRELATED, + }); + expect(script).toMatchObject({ + status: "unmatched", + basis: "none", + confidence: "high", + reason: "no-static-content-match", + candidate_static_count: 0, + }); + expect(result.summary.unmatched).toBe(1); + }, +); + +it.each([ + ["only the second", ["none", "explicit"], "unmatched"], + ["only the first", ["explicit", "none"], "unmatched"], + ["neither", ["none", "none"], "unknown"], +] as const)( + "classifies an unmatched captured script when %s of two layers maps it", + async (_label, mappings, status) => { + const live = await createTestTempDirectory("rea-runtime-layer-mapping-"); + const { script } = await reconcileCapturedScript({ + layers: await Promise.all( + mappings.map(async (mapping) => ({ + root: await applicationFixture(), + mapping, + })), + ), + live, + source: UNRELATED, + }); + expect(script).toMatchObject({ + status, + reason: + status === "unknown" + ? "no-authorized-location-mapping" + : "no-static-content-match", + }); + }, +); + +it.each([ + [1, "matched", "unique-content-match"], + [2, "ambiguous", "ambiguous-static-candidates"], +] as const)( + "keeps unmapped content-only correspondence with %i digest candidates", + async (copies, status, reason) => { + const live = await createTestTempDirectory("rea-runtime-unmapped-content-"); + const { result, script } = await reconcileCapturedScript({ + layers: await Promise.all( + Array.from({ length: copies }, async () => { + const root = await applicationFixture(); + await writeFile(join(root, "other.js"), CAPTURED); + return { root, mapping: "none" as const }; + }), + ), + live, + source: CAPTURED, + }); + expect(script).toMatchObject({ + status, + basis: copies === 1 ? "content-sha256" : "none", + reason, + candidate_static_count: copies, + }); + expect(result.summary.unmatched).toBe(0); + }, +); + +it.each([ + ["https://example.test/assets/absent.js", "unmatched"], + ["https://other.test/assets/absent.js", "unknown"], +] as const)( + "applies URL-prefix mapping presence to captured script %s", + async (url, status) => { + const root = await applicationFixture(); + const parsed = parseRuntimeReconciliationInput( + reconcileJavaScriptRuntimeInputSchema.parse({ + static_layers: [ + { + role: "application", + analysis: await analyzeFixture(root), + runtime_mappings: [ + { + kind: "url-prefix", + prefix: "https://example.test/assets/", + artifact_prefix: "", + }, + ], + }, + ], + runtime_observations: [ + electronRuntimeEvidence(root, UNRELATED, { includeWorker: false }), + ], + }), + ); + // Model a URL-located capture at the matcher boundary; browser and + // Inspector projections supply URL locations to the same classifier. + const entities = projectRuntimeCaptures(parsed.captures).entities.map( + (entity) => + entity.kind === "script" + ? { ...entity, location: { kind: "url" as const, value: url } } + : entity, + ); + const result = reconcileRuntimeEntities({ + entities, + candidates: collectStaticRuntimeCandidates(parsed.layers), + layers: parsed.layers, + }); + expect( + result.items.find(({ entity_kind }) => entity_kind === "script"), + ).toMatchObject({ + status, + confidence: status === "unknown" ? "unknown" : "high", + reason: + status === "unknown" + ? "no-authorized-location-mapping" + : "no-static-content-match", + }); + }, +);