Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6cda443c1 | ||
|
|
e308f35c43 |
No files matched your search
@@ -106,7 +106,12 @@ nodes. REA evaluates compatible static candidates conservatively:
|
|||||||
that disagreement. If a mapped candidate's digest is unavailable and no
|
that disagreement. If a mapped candidate's digest is unavailable and no
|
||||||
mapped content match is known, the comparison remains `unknown`.
|
mapped content match is known, the comparison remains `unknown`.
|
||||||
6. Missing authorization mappings and incomplete or truncated input coverage
|
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 content match, is `unknown` with
|
||||||
|
`no-authorized-location-mapping`, with or without captured bytes; several
|
||||||
|
content matches remain `ambiguous` under rule 4. Only a mapped location can
|
||||||
|
be `unmatched` with `no-static-content-match`.
|
||||||
|
|
||||||
Every successful cross-layer `observed_as` edge has
|
Every successful cross-layer `observed_as` edge has
|
||||||
`authority: cross-layer-reconciliation` and `state: inferred`. The underlying
|
`authority: cross-layer-reconciliation` and `state: inferred`. The underlying
|
||||||
|
|||||||
@@ -77,14 +77,13 @@ const reconcileEntity = (
|
|||||||
reason: "runtime-location-unresolved",
|
reason: "runtime-location-unresolved",
|
||||||
candidates: [],
|
candidates: [],
|
||||||
});
|
});
|
||||||
const locations = candidatesWithLocation(entity, index, layers);
|
const { locations, mapped } = locateEntity(entity, index, layers);
|
||||||
if (entity.sourceSha256 !== null)
|
if (entity.sourceSha256 !== null)
|
||||||
return reconcileCapturedSource(
|
return reconcileCapturedSource(entity, entity.sourceSha256, {
|
||||||
entity,
|
|
||||||
entity.sourceSha256,
|
|
||||||
locations,
|
locations,
|
||||||
|
mapped,
|
||||||
index,
|
index,
|
||||||
);
|
});
|
||||||
|
|
||||||
const location = locations.at(0);
|
const location = locations.at(0);
|
||||||
if (locations.length === 1 && location !== undefined) {
|
if (locations.length === 1 && location !== undefined) {
|
||||||
@@ -103,20 +102,40 @@ const reconcileEntity = (
|
|||||||
reason: "ambiguous-static-candidates",
|
reason: "ambiguous-static-candidates",
|
||||||
candidates: locations.map(({ candidate }) => candidate),
|
candidates: locations.map(({ candidate }) => candidate),
|
||||||
});
|
});
|
||||||
const mapped = layers.some(
|
return withoutStaticCandidate(entity, mapped, "unknown");
|
||||||
(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: [],
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
interface LocatedCandidate {
|
||||||
readonly candidate: StaticRuntimeCandidate;
|
readonly candidate: StaticRuntimeCandidate;
|
||||||
readonly mapping: MappedRuntimePath;
|
readonly mapping: MappedRuntimePath;
|
||||||
@@ -125,8 +144,15 @@ interface LocatedCandidate {
|
|||||||
const reconcileCapturedSource = (
|
const reconcileCapturedSource = (
|
||||||
entity: RuntimeReconciliationEntity,
|
entity: RuntimeReconciliationEntity,
|
||||||
sourceSha256: string,
|
sourceSha256: string,
|
||||||
locations: readonly LocatedCandidate[],
|
{
|
||||||
index: CandidateIndex,
|
locations,
|
||||||
|
mapped,
|
||||||
|
index,
|
||||||
|
}: {
|
||||||
|
readonly locations: readonly LocatedCandidate[];
|
||||||
|
readonly mapped: boolean;
|
||||||
|
readonly index: CandidateIndex;
|
||||||
|
},
|
||||||
): EvaluatedMatch => {
|
): EvaluatedMatch => {
|
||||||
// A known mapped candidate owns the comparison. Unrelated equal bytes cannot
|
// A known mapped candidate owns the comparison. Unrelated equal bytes cannot
|
||||||
// repair a disagreement or establish the content of a candidate without a digest.
|
// 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),
|
candidates: locations.map(({ candidate: located }) => located),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return unresolved(entity, {
|
return withoutStaticCandidate(entity, mapped, "high");
|
||||||
status: "unmatched",
|
|
||||||
basis: "none",
|
|
||||||
confidence: "high",
|
|
||||||
reason: "no-static-content-match",
|
|
||||||
candidates: [],
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const candidatesWithLocation = (
|
const locateEntity = (
|
||||||
entity: RuntimeReconciliationEntity,
|
entity: RuntimeReconciliationEntity,
|
||||||
index: CandidateIndex,
|
index: CandidateIndex,
|
||||||
layers: readonly ParsedStaticLayer[],
|
layers: readonly ParsedStaticLayer[],
|
||||||
): LocatedCandidate[] => {
|
): { readonly locations: LocatedCandidate[]; readonly mapped: boolean } => {
|
||||||
const located = new Map<string, LocatedCandidate>();
|
const located = new Map<string, LocatedCandidate>();
|
||||||
|
let mapped = false;
|
||||||
for (const layer of layers)
|
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 [
|
for (const candidate of [
|
||||||
...(index.byLayerCategoryPath.get(
|
...(index.byLayerCategoryPath.get(
|
||||||
layerCategoryPathKey(layer.layerId, entity.staticKind, mapping.path),
|
layerCategoryPathKey(layer.layerId, entity.staticKind, mapping.path),
|
||||||
@@ -215,12 +237,16 @@ const candidatesWithLocation = (
|
|||||||
const key = candidateKey(candidate);
|
const key = candidateKey(candidate);
|
||||||
if (!located.has(key)) located.set(key, { candidate, mapping });
|
if (!located.has(key)) located.set(key, { candidate, mapping });
|
||||||
}
|
}
|
||||||
return [...located.values()].sort((left, right) =>
|
}
|
||||||
compareUnicodeCodePoints(
|
return {
|
||||||
candidateKey(left.candidate),
|
locations: [...located.values()].sort((left, right) =>
|
||||||
candidateKey(right.candidate),
|
compareUnicodeCodePoints(
|
||||||
|
candidateKey(left.candidate),
|
||||||
|
candidateKey(right.candidate),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
mapped,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const indexCandidates = (
|
const indexCandidates = (
|
||||||
|
|||||||
+201
@@ -229,3 +229,204 @@ it("keeps unavailable mapped digests unknown instead of borrowing unrelated matc
|
|||||||
reason: "static-or-runtime-coverage-incomplete",
|
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