fix(javascript): share source-map resolution and preserve path uncertainty

This commit is contained in:
morluto
2026-10-13 13:48:24 +08:00
parent 73dd8d6ed5
commit 3ae826ceba
16 changed files with 551 additions and 67 deletions
+6 -3
View File
@@ -156,9 +156,12 @@ to the comparison Evidence in a live session.
`compare_source_to_bundle` compares one cryptographically committed
`HistoricalSourceGraph` with authenticated application-graph Evidence. The
stable scoring model reports every admitted signal and weight: exact source
digest, source-map original path, exact current path, path suffix, basename, and
language extension. Exact digest wins over location inference; weak basename
or extension evidence never forces a mapping.
digest, source-map original path, exact current path, path suffix, Unicode-
equivalent path, basename, and language extension. Exact digest wins over
location inference; weak basename, extension, and Unicode-equivalence evidence
never forces a mapping. Unicode-equivalent spellings are candidate evidence
only because distinct filesystem paths can preserve those spellings as separate
files; multiple canonical-equivalent candidates remain unknown.
Each historical source file is classified as `unchanged`, `modified`,
`removed`, `split`, `merged`, `duplicated`, or `unknown`. Multiple equally
@@ -0,0 +1,71 @@
import { expect, it } from "vitest";
import { TraceMap } from "@jridgewell/trace-mapping";
import { analyzeJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
const HASH = "a".repeat(64);
it("retains batched TraceMap-resolved paths and null source positions", () => {
const sourceMapText = JSON.stringify({
version: 3,
file: "main.js",
sourceRoot: "../src/",
sources: ["cafe\u0301.ts", null, "other.ts"],
sourcesContent: ["export const value = 1;", null, "export {};"],
names: [],
mappings: "",
});
const analysis = analyzeJavaScriptArtifactFiles({
files: [
{
path: "dist/main.js.map",
container_sha256: HASH,
sha256: HASH,
bytes: Buffer.byteLength(sourceMapText),
inventory_artifact_id: "artifact_test",
kind: "source-map",
unpacked: false,
text: { included: true, value: sourceMapText },
},
],
containers: [],
text_bytes_read: Buffer.byteLength(sourceMapText),
invalid_utf8_files: 0,
});
const originals = analysis.source_maps[0]?.sources;
const original = originals?.[0];
const producerPath = new TraceMap(
{
version: 3,
file: "main.js",
sourceRoot: "../src/",
sources: ["cafe\u0301.ts", "", "other.ts"],
names: [],
mappings: "",
},
"file:///artifact/dist/main.js.map",
).resolvedSources[0];
if (producerPath === undefined)
throw new Error("TraceMap should resolve its single source declaration");
expect(originals?.[1]?.reference).toBeNull();
expect(originals?.[2]?.reference?.resolution).toEqual({
kind: "artifact-relative",
path: "src/other.ts",
});
expect(original?.reference).toMatchObject({
source_name: "cafe\u0301.ts",
source_root: "../src/",
map_path: "dist/main.js.map",
resolution: {
kind: "artifact-relative",
path: "src/cafe\u0301.ts",
},
});
expect(original?.reference?.resolution).toEqual({
kind: "artifact-relative",
path: decodeURIComponent(
new URL(producerPath).pathname.slice("/artifact/".length),
),
});
});
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
import { moduleKindForJavaScriptReference } from "./JavaScriptArtifactPathResolution.js";
import { setImmediate } from "node:timers/promises";
import { resolveJavaScriptSourceMapReference } from "../../domain/javascript/javascriptSourceMapPaths.js";
import { resolveJavaScriptSourceMapReferences } from "../../domain/javascript/javascriptSourceMapPaths.js";
import { analyzeParsedJavaScriptStaticSourceSteps } from "../../domain/javascript/javascriptStaticAnalysis.js";
import {
analyzeParsedJavaScriptSemanticsSteps,
@@ -561,21 +561,27 @@ const collectSourceMapOriginals = (
"Source map sourceRoot must be a string or null.",
);
const root = typeof map.sourceRoot === "string" ? map.sourceRoot : null;
for (const [index, raw] of names.entries()) {
const sourceNames: (string | null)[] = [];
for (const raw of names) {
if (raw !== null && typeof raw !== "string")
return invalidSourceMap(
file,
"Source map contains a non-string source name.",
);
sourceNames.push(raw);
}
const references = resolveJavaScriptSourceMapReferences(
sourceNames,
root,
file.path,
);
for (const [index, reference] of references.entries()) {
const rawContent = Array.isArray(contents) ? contents[index] : undefined;
const content = typeof rawContent === "string" ? rawContent : null;
sources.push({
source_index: index,
declaration_index: declarationIndex++,
reference:
typeof raw === "string"
? resolveJavaScriptSourceMapReference(raw, root, file.path)
: null,
reference,
content,
content_sha256: content === null ? null : sha256(content),
});
+6 -2
View File
@@ -2,7 +2,6 @@ import * as t from "@babel/types";
import {
decodeValidatedSourceMapLeaves,
isBeforeSourceMapLeafStop,
resolveSourceMapSource,
} from "../javascript/sourceMaps/DecodedSourceMap.js";
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
@@ -21,6 +20,7 @@ import { webSourceMapsSchema } from "../domain/webBundleAnalysis.js";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import { safeParseJson } from "../domain/safeJson.js";
import { sourceMapJsonText } from "../domain/sourceMapEnvelope.js";
import { resolveJavaScriptSourceMapUrl } from "../domain/javascript/javascriptSourceMapPaths.js";
import { jsonParts } from "../domain/jsonSerialization.js";
import {
inspectSourceMapValue,
@@ -867,7 +867,11 @@ const preflightResolvedSourceBytes = (
const sourceKey = source;
let resolved = sourcesForRoot.get(sourceKey);
if (resolved === undefined)
resolved = resolveSourceMapSource(source, map.sourceRoot, mapUrl);
resolved = resolveJavaScriptSourceMapUrl(
source,
map.sourceRoot,
mapUrl,
);
const bytes = Buffer.byteLength(resolved, "utf8");
if (bytes > SOURCE_MAP_RESPONSE_BYTES - resolvedBytes)
throw resolvedSourceLimitFailure();
+3 -3
View File
@@ -1,6 +1,5 @@
import { expect, it } from "vitest";
import { resolveJavaScriptSourceMapReference } from "../javascript/javascriptSourceMapPaths.js";
import { functionDossierSchema } from "../hopperValues.js";
import { ghidraFunctionDossier } from "../ghidraValues.fixture.js";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
@@ -24,13 +23,14 @@ import {
completeWithinViewCoverage,
pageViewCoverage,
} from "./analysisViewCoverage.js";
import { sourceMapReference } from "../javascript/javascriptApplicationGraph.fixture.js";
import { javascriptModulePath } from "./javascriptApplicationView.js";
import {
inspectAnalysisViewInputSchema,
projectAnalysisView,
type AnalysisViewParent,
} from "./analysisView.js";
const layoutParent = (): AnalysisViewParent => {
const evidence = analysisViewLayoutEvidence();
return {
@@ -532,7 +532,7 @@ it("pages actual modules, including unknown and empty paths, without unrelated r
observations: original.observations.map((observation) => ({
...(kind === "source-module"
? {
source_map_reference: resolveJavaScriptSourceMapReference(
source_map_reference: sourceMapReference(
"",
null,
"renderer.js",
@@ -6,6 +6,7 @@ import {
} from "./javascriptApplicationGraph.js";
import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js";
import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js";
import { resolveJavaScriptSourceMapReferences } from "./javascriptSourceMapPaths.js";
export const APPLICATION_GRAPH_DIGESTS = {
package: "1".repeat(64),
@@ -22,6 +23,20 @@ export const completeCoverage = {
limits: [],
} satisfies ApplicationGraphEvidence["coverage"];
export const sourceMapReference = (
sourceName: string,
sourceRoot: string | null,
mapPath: string,
) => {
const reference = resolveJavaScriptSourceMapReferences(
[sourceName],
sourceRoot,
mapPath,
)[0];
if (reference == null) throw new TypeError("Expected a source-map reference");
return reference;
};
const extractor = (operation: string) => ({
name: "rea-test-fixture",
version: "1.0.0",
@@ -20,10 +20,9 @@ import {
inferredArtifactEvidence,
nodeByLabel,
runtimeEvidence,
sourceMapReference,
unknownEvidence,
} from "./javascriptApplicationGraph.fixture.js";
import { resolveJavaScriptSourceMapReference } from "./javascriptSourceMapPaths.js";
const createLargeNativeExportGraph = (): JavaScriptApplicationGraph => {
const properties = Object.fromEntries(
Array.from({ length: 1_001 }, (_, index) => [
@@ -438,7 +437,7 @@ describe("JavaScript Application Graph: source maps and observations", () => {
observations: [
{
label: "src/editor.ts",
source_map_reference: resolveJavaScriptSourceMapReference(
source_map_reference: sourceMapReference(
"webpack:///src/editor.ts",
null,
"dist/app.js.map",
@@ -535,7 +534,7 @@ describe("JavaScript Application Graph: source-map digest consistency", () => {
observations: [
{
label: null,
source_map_reference: resolveJavaScriptSourceMapReference(
source_map_reference: sourceMapReference(
"src/editor.ts",
null,
"dist/app.js.map",
@@ -1,5 +1,6 @@
import { posix } from "node:path";
import { domainToASCII, fileURLToPath, pathToFileURL } from "node:url";
import { TraceMap } from "@jridgewell/trace-mapping";
import { z } from "zod";
@@ -25,22 +26,64 @@ export type JavaScriptSourceMapReference = z.infer<
typeof javascriptSourceMapReferenceSchema
>;
/** Interpret a declaration once at the source-map ingestion boundary. */
export const resolveJavaScriptSourceMapReference = (
sourceName: string,
/** Interpret declarations together once at their source-map ingestion boundary. */
export const resolveJavaScriptSourceMapReferences = (
sourceNames: readonly (string | null)[],
sourceRoot: string | null,
mapPath: string,
): JavaScriptSourceMapReference => {
// TraceMap's producer prefixes sourceRoot before resolving the URL reference.
const reference = `${sourceRoot ? `${sourceRoot}/` : ""}${sourceName}`;
return {
source_name: sourceName,
source_root: sourceRoot,
map_path: mapPath,
resolution: resolveJavaScriptSourceMapPath(reference, mapPath),
};
): (JavaScriptSourceMapReference | null)[] => {
if (sourceNames.length === 0) return [];
const resolvedSources = resolveSourceMapUrls(
sourceNames.map((sourceName) => sourceName ?? ""),
sourceRoot,
"",
);
return sourceNames.map((sourceName, index) => {
if (sourceName === null) return null;
const reference = resolvedSources[index];
if (reference === undefined)
throw new TypeError(
`TraceMap did not resolve source declaration ${String(index)}`,
);
return {
source_name: sourceName,
source_root: sourceRoot,
map_path: mapPath,
resolution: resolveJavaScriptSourceMapPath(reference, mapPath),
};
});
};
/** Resolve a source declaration using the same pinned format implementation as map consumers. */
export const resolveJavaScriptSourceMapUrl = (
source: string | null,
sourceRoot: string | null | undefined,
mapUrl: string,
): string => {
const resolved = resolveSourceMapUrls([source ?? ""], sourceRoot, mapUrl)[0];
if (resolved === undefined)
throw new TypeError(
"TraceMap did not resolve its single source declaration",
);
return resolved;
};
const resolveSourceMapUrls = (
sources: string[],
sourceRoot: string | null | undefined,
mapUrl: string,
): readonly string[] =>
new TraceMap(
{
version: 3,
names: [],
sources,
...(sourceRoot == null ? {} : { sourceRoot }),
mappings: "",
},
mapUrl,
).resolvedSources;
/** Resolve a source-map URL reference without changing its recorded identity. */
const resolveJavaScriptSourceMapPath = (
source: string,
@@ -1,10 +1,11 @@
import { describe, expect, it } from "vitest";
import { TraceMap } from "@jridgewell/trace-mapping";
import { resolveJavaScriptSourceMapReference } from "./javascriptSourceMapPaths.js";
import {
createJavaScriptApplicationGraph,
createJavaScriptApplicationNode,
} from "./javascriptApplicationGraph.js";
import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js";
import { createHistoricalSourceGraph } from "../referenceSourceGraph.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { compareSourceToBundle } from "./sourceToBundleComparison.js";
@@ -12,7 +13,10 @@ import {
SOURCE_TO_BUNDLE_SIGNAL_WEIGHTS,
sourceToBundleComparisonResultSchema,
} from "./sourceToBundleComparisonSchemas.js";
import { artifactEvidence } from "./javascriptApplicationGraph.fixture.js";
import {
artifactEvidence,
sourceMapReference,
} from "./javascriptApplicationGraph.fixture.js";
const HASH = {
artifact: "1".repeat(64),
@@ -42,9 +46,6 @@ describe("historical source to bundle comparison", () => {
const second = compareSourceToBundle(input);
expect(first).toEqual(second);
expect(() =>
sourceToBundleComparisonResultSchema.parse(first),
).not.toThrow();
expect(first.summary).toEqual({
unchanged: 1,
modified: 0,
@@ -153,7 +154,7 @@ describe("source to bundle digest changes", () => {
observations: [
{
label: "display label",
source_map_reference: resolveJavaScriptSourceMapReference(
source_map_reference: sourceMapReference(
"webpack://fixture/./src/modified.ts",
null,
"dist/main.js.map",
@@ -202,6 +203,90 @@ describe("source to bundle digest changes", () => {
});
});
describe("source to bundle Unicode path collisions", () => {
it("keeps colliding canonical-equivalent current paths ambiguous", () => {
const historicalPath = "src/a\u0301\u0323.ts";
const currentPaths = ["src/a\u0323\u0301.ts", "src/\u1ea1\u0301.ts"];
const nodes = currentPaths.map((path, index) =>
moduleNode(`unicode-${index}`, { path }),
);
const result = comparePathToNodes(historicalPath, nodes);
const mapping = item(result, historicalPath);
expect(mapping).toMatchObject({
status: "unknown",
confidence: "unknown",
current_node_ids: [],
candidates: [
expect.objectContaining({
signals: expect.arrayContaining([
expect.objectContaining({ kind: "unicode-equivalent-path" }),
]),
}),
expect.objectContaining({
signals: expect.arrayContaining([
expect.objectContaining({ kind: "unicode-equivalent-path" }),
]),
}),
],
limitations: [
expect.stringContaining(
"filesystem paths with distinct Unicode spellings",
),
],
});
});
it("prefers a raw source-map path over canonically equivalent alternatives", () => {
const historicalPath = "src/cafe\u0301.ts";
const exact = sourceMapNode(
"exact",
"cafe\u0301.ts",
"../src/",
HASH.modified,
);
const equivalent = sourceMapNode(
"equivalent",
"caf\u00e9.ts",
"../src/",
HASH.removed,
);
const result = comparePathToNodes(historicalPath, [exact, equivalent]);
expect(item(result, historicalPath)).toMatchObject({
status: "modified",
current_node_ids: [exact.node_id],
});
});
it("finds Unicode-equivalent suffixes of absolute source-map URLs", () => {
const historicalPath = "src/caf\u00e9.ts";
const node = sourceMapNode(
"remote",
"cafe\u0301.ts",
"https://cdn.example/src/",
HASH.modified,
);
const result = comparePathToNodes(historicalPath, [node]);
expect(item(result, historicalPath)).toMatchObject({
status: "unknown",
current_node_ids: [],
candidates: [
expect.objectContaining({
current_node_id: node.node_id,
signals: expect.arrayContaining([
expect.objectContaining({
kind: "unicode-equivalent-path",
current_values: ["cdn.example/src/cafe\u0301.ts"],
}),
]),
}),
],
});
});
});
describe("source to bundle path syntax", () => {
it.each([
"src/modified#part.ts",
@@ -282,9 +367,60 @@ describe("source to bundle path syntax", () => {
}),
],
});
expect(
item(result, "src/modified.ts").candidates.flatMap(
({ signals }) => signals,
),
).not.toContainEqual(
expect.objectContaining({ kind: "unicode-equivalent-path" }),
);
});
});
describe("source-map resolver equivalence", () => {
it.each([
{
sourceRoot: "https://cdn.example/src/",
expectedSource: "https://cdn.example/src/a.js",
expectedResolution: { kind: "suffix", path: "cdn.example/src/a.js" },
},
{
sourceRoot: "https://cdn.example/src//",
expectedSource: "https://cdn.example/src/a.js",
expectedResolution: { kind: "suffix", path: "cdn.example/src/a.js" },
},
{
sourceRoot: "webpack://",
expectedSource: "webpack:///a.js",
expectedResolution: { kind: "suffix", path: "a.js" },
},
])(
"matches TraceMap for sourceRoot $sourceRoot",
({ sourceRoot, expectedSource, expectedResolution }) => {
const producerSource = new TraceMap(
{
version: 3,
file: "main.js",
sourceRoot,
sources: ["a.js"],
names: [],
mappings: "",
},
"file:///artifact/dist/main.js.map",
).resolvedSources[0];
const reference = sourceMapReference(
"a.js",
sourceRoot,
"dist/main.js.map",
);
expect(producerSource).toBe(expectedSource);
expect(reference.source_root).toBe(sourceRoot);
expect(reference.resolution).toEqual(expectedResolution);
},
);
});
describe("source to bundle complete metadata", () => {
it("accepts long source metadata and all reported limitations", () => {
const result = compareSourceToBundle({
@@ -328,6 +464,61 @@ describe("source to bundle complete metadata", () => {
});
});
const sourceMapNode = (
key: string,
sourceName: string,
sourceRoot: string,
digest: string,
) =>
createJavaScriptApplicationNode({
kind: "source-module",
identity: {
strategy: "source-map-original",
source_root: sourceRoot,
stability: "source-map-exact",
source_map_sha256: HASH.artifact,
original_source: sourceName,
source_sha256: digest,
},
observations: [
{
label: `${key} source-map spelling`,
source_map_reference: sourceMapReference(
sourceName,
sourceRoot,
`dist/${key}.map`,
),
properties: { content_available: true, content_sha256: digest },
evidence: artifactEvidence(HASH.artifact, `dist/${key}.map`),
},
],
});
const comparePathToNodes = (
sourcePath: string,
nodes: readonly ApplicationNode[],
) =>
compareSourceToBundle({
reference: historicalGraph("complete", [sourcePath]),
application: {
evidenceId: EVIDENCE_ID,
rootArtifactSha256: HASH.artifact,
graph: createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids: nodes.map(({ node_id: nodeId }) => nodeId),
nodes,
edges: [],
coverage: {
status: "complete",
truncated: false,
omitted_count: 0,
limits: [],
},
limitations: [],
}),
},
});
const historicalGraph = (
inventoryState: "complete" | "partial",
paths = [
@@ -258,7 +258,11 @@ const classifyMapping = (
confidence: "unknown",
currentNodeIds: [],
limitations: [
"Only weak basename or extension signals were available; no mapping was forced.",
candidates.some((candidate) =>
hasSignal(candidate, "unicode-equivalent-path"),
)
? "Unicode-equivalent paths are inferred candidates only; filesystem paths with distinct Unicode spellings may identify different files, so no mapping was forced."
: "Only weak basename or extension signals were available; no mapping was forced.",
],
};
return absenceComplete
@@ -15,6 +15,7 @@ export const SOURCE_TO_BUNDLE_SIGNAL_WEIGHTS = [
["source-map-original-path", 70],
["current-path-exact", 60],
["current-path-suffix", 40],
["unicode-equivalent-path", 20],
["basename-match", 20],
["language-extension", 5],
] as const;
@@ -24,6 +25,7 @@ const sourceToBundleSignalKindSchema = z.enum([
"source-map-original-path",
"current-path-exact",
"current-path-suffix",
"unicode-equivalent-path",
"basename-match",
"language-extension",
]);
+38 -1
View File
@@ -102,8 +102,12 @@ export const buildSourceToBundleCandidateIndex = (
for (const path of projection.paths) {
for (const suffix of path.allowSuffix
? pathSuffixes(path.value)
: [path.value])
: [path.value]) {
addIndexValue(byPathSuffix, suffix, projection.node.node_id);
const canonical = suffix.normalize("NFC");
if (canonical !== suffix)
addIndexValue(byPathSuffix, canonical, projection.node.node_id);
}
addIndexValue(
byBasename,
posix.basename(path.value),
@@ -126,9 +130,13 @@ export const candidateIdsForSource = (
source: SourceFile,
index: SourceToBundleCandidateIndex,
): string[] => {
const canonicalPath = source.path.normalize("NFC");
const groups = [
source.sha256 === null ? undefined : index.byDigest.get(source.sha256),
index.byPathSuffix.get(source.path),
canonicalPath === source.path
? undefined
: index.byPathSuffix.get(canonicalPath),
index.byBasename.get(posix.basename(source.path)),
];
const seen = new Set<string>();
@@ -220,6 +228,14 @@ const candidateSignals = (
signals.push(
signal("source-map-original-path", source.path, sourceMapPaths),
);
const unicodeEquivalentPaths = matchingUnicodePaths(
source.path,
current.paths,
);
if (unicodeEquivalentPaths.length > 0)
signals.push(
signal("unicode-equivalent-path", source.path, unicodeEquivalentPaths),
);
const otherPaths = current.paths.filter(
({ kind }) => kind !== "source-map-original",
);
@@ -286,6 +302,27 @@ const matchingPaths = (
)
.map(({ value }) => value);
const matchingUnicodePaths = (
sourcePath: string,
currentPaths: readonly CurrentPath[],
): string[] => {
const canonicalSource = sourcePath.normalize("NFC");
return currentPaths
.filter(({ value, allowSuffix }) => {
if (
value === sourcePath ||
(allowSuffix && value.endsWith(`/${sourcePath}`))
)
return false;
const canonicalValue = value.normalize("NFC");
return (
canonicalValue === canonicalSource ||
(allowSuffix && canonicalValue.endsWith(`/${canonicalSource}`))
);
})
.map(({ value }) => value);
};
const addJsonPath = (paths: CurrentPath[], value: unknown): void => {
if (typeof value === "string") addPath(paths, "observation-path", value);
};
@@ -19,31 +19,6 @@ export const isBeforeSourceMapLeafStop = (
);
};
/** Resolve one declaration with the same pinned upstream implementation used by full maps. */
export const resolveSourceMapSource = (
source: string | null,
sourceRoot: string | null | undefined,
mapUrl: string,
): string => {
const map = new TraceMap(
{
version: 3,
names: [],
sources: [source ?? ""],
...(sourceRoot == null ? {} : { sourceRoot }),
mappings: "",
},
mapUrl,
);
const resolved = map.resolvedSources[0];
if (resolved === undefined)
throw new SourceMapFormatFailure(
"format",
"Source-map resolver did not return the requested source identity.",
);
return resolved;
};
/** Decode leaf mappings and validate producer indices before any consumer publishes them. */
export const decodeValidatedSourceMapLeaves = (
leaves: readonly SourceMapLeaf[],
@@ -6,7 +6,7 @@ import { analyzeJavaScriptApplication } from "../../support/javascriptApplicatio
import { parseApplicationGraphEvidence } from "../../../src/application/javascript/JavaScriptApplicationEvidenceGraph.js";
import { findApplicationFeatureSeeds } from "../../../src/domain/javascript/javascriptFeatureSeed.js";
import { compareSourceToBundle } from "../../../src/domain/javascript/sourceToBundleComparison.js";
import { resolveSourceMapSource } from "../../../src/javascript/sourceMaps/DecodedSourceMap.js";
import { resolveJavaScriptSourceMapUrl } from "../../../src/domain/javascript/javascriptSourceMapPaths.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { importReferenceSource } from "../../support/referenceSourceResourceScope.js";
@@ -180,7 +180,7 @@ it.each([
decoy: decoyPath = "src/a.js",
}) => {
if (sourceRoot !== undefined) {
const resolved = resolveSourceMapSource(
const resolved = resolveJavaScriptSourceMapUrl(
source,
sourceRoot,
`https://example.test/${mapPath}`,
@@ -2,6 +2,7 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import type { Client } from "@modelcontextprotocol/client";
import { describe, expect, it } from "vitest";
import { Ajv2020 } from "ajv/dist/2020.js";
import type { AnySchema } from "ajv";
import type { BinarySession } from "../../../src/application/binary/BinarySession.js";
import {
@@ -10,12 +11,143 @@ import {
SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
} from "../../../src/contracts/javascript/javascriptApplicationWorkflowExamples.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import {
createJavaScriptApplicationGraph,
createJavaScriptApplicationNode,
} from "../../../src/domain/javascript/javascriptApplicationGraph.js";
import { createJavaScriptSemanticGraph } from "../../../src/domain/javascript/javascriptSemanticGraph.js";
import {
artifactEvidence,
sourceMapReference,
} from "../../../src/domain/javascript/javascriptApplicationGraph.fixture.js";
import {
createApplicationMcpHarness,
type ApplicationMcpHarness,
} from "../../fixtures/applicationMcpHarness.js";
import { createHistoricalSourceGraph } from "../../../src/domain/referenceSourceGraph.js";
async function runUnicodeSourceMapComparison(
harness: ApplicationMcpHarness,
): Promise<void> {
const baseApplication = SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.application;
const subject = baseApplication.subject;
if (subject === null) throw new Error("Application Evidence has no subject");
const baseResult = javascriptApplicationAnalysisResultSchema.parse(
baseApplication.normalized_result,
);
const baseGraph = baseResult.graph;
const decomposedSource = "cafe\u0301.ts";
const unicodeSourceNode = createJavaScriptApplicationNode({
kind: "source-module",
identity: {
strategy: "source-map-original",
source_root: "../src/",
stability: "source-map-exact",
source_map_sha256: "1".repeat(64),
original_source: decomposedSource,
source_sha256: "3".repeat(64),
},
observations: [
{
label: "decomposed source-map spelling",
source_map_reference: sourceMapReference(
decomposedSource,
"../src/",
"dist/main.js.map",
),
properties: { content_available: true, content_sha256: "3".repeat(64) },
evidence: artifactEvidence("1".repeat(64), "dist/main.js.map"),
},
],
});
const applicationGraph = createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids: [...baseGraph.root_node_ids, unicodeSourceNode.node_id],
nodes: [...baseGraph.nodes, unicodeSourceNode],
edges: baseGraph.edges,
coverage: baseGraph.coverage,
limitations: baseGraph.limitations,
});
const semantic = baseResult.semantic_graph;
const { graph_id: _semanticGraphId, ...semanticInput } = semantic;
const semanticGraph = createJavaScriptSemanticGraph({
...semanticInput,
application_graph_id: applicationGraph.graph_id,
});
const unicodeApplication = createEvidence(
{
path: baseResult.input_path,
sha256: subject.digest.sha256,
format: subject.format,
},
baseApplication.provider,
{
predicateType: baseApplication.predicate_type,
operation: baseApplication.operation,
parameters: baseApplication.parameters,
result: {
...baseResult,
graph: applicationGraph,
semantic_graph: semanticGraph,
},
confidence: baseApplication.confidence,
authority: baseApplication.authority,
limitations: baseApplication.limitations,
evidenceLinks: baseApplication.evidence_links,
},
);
const historicalUnicodePath = "src/caf\u00e9.ts";
const unicodeReference = createHistoricalSourceGraph({
...SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference,
root_sha256: undefined,
entries: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference.entries.map(
(entry) =>
entry.path === "src/main.ts"
? { ...entry, path: historicalUnicodePath }
: entry,
),
});
const reply = await harness.client.callTool({
name: "compare_source_to_bundle",
arguments: { reference: unicodeReference, application: unicodeApplication },
});
expect(reply.isError).not.toBe(true);
expect(reply.structuredContent).toMatchObject({
normalized_result: {
items: [
{
source_path: historicalUnicodePath,
status: "unknown",
current_node_ids: [],
candidates: [
expect.objectContaining({
confidence: "low",
signals: expect.arrayContaining([
expect.objectContaining({
kind: "unicode-equivalent-path",
source_value: historicalUnicodePath,
current_values: ["src/cafe\u0301.ts"],
}),
]),
}),
],
},
],
},
});
const advertised = (await harness.client.listTools()).tools.find(
({ name }) => name === "compare_source_to_bundle",
);
if (advertised?.outputSchema === undefined)
throw new Error("Comparison tool output schema is missing");
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const outputSchema = advertised.outputSchema as AnySchema;
expect(ajv.validateSchema(outputSchema)).toBe(true);
const validateOutput = ajv.compile(outputSchema);
expect(validateOutput(reply.structuredContent)).toBe(true);
}
async function runInlineWorkflowScenarios(
harness: ApplicationMcpHarness,
): Promise<void> {
@@ -92,6 +224,8 @@ async function runInlineWorkflowScenarios(
},
});
await runUnicodeSourceMapComparison(harness);
const referenceWithExclusion = createHistoricalSourceGraph({
...SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference,
root_sha256: undefined,
@@ -73,9 +73,9 @@ processTest.for(cases)(
[
"--input-type=module",
"--eval",
`import { resolveJavaScriptSourceMapReference } from "./dist/domain/javascript/javascriptSourceMapPaths.js";
`import { resolveJavaScriptSourceMapReferences } from "./dist/domain/javascript/javascriptSourceMapPaths.js";
const [source, root] = JSON.parse(process.argv[1]);
console.log(JSON.stringify(resolveJavaScriptSourceMapReference(source, root, "maps/main.js.map")));`,
console.log(JSON.stringify(resolveJavaScriptSourceMapReferences([source], root, "maps/main.js.map")[0]));`,
JSON.stringify([scenario.source, sourceRoot]),
],
{ timeoutMs: 10_000 },