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 <noreply@anthropic.com>
This commit is contained in:
1 parent
719d56ebfe
commit
e308f35c43
3 files changed
+266
-35
No files matched your search
@@ -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
|
||||
|
||||
@@ -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<string, LocatedCandidate>();
|
||||
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 = (
|
||||
|
||||
+201
@@ -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",
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user