Files
rea/src/domain/webBundleAnalyzerResult.ts
T
morluto b58adafa4f fix(javascript): align module lookup and source evidence
Preserve CommonJS literal paths and confined ESM URL semantics, including package main fallback, directory intent, Unicode identity, and present opaque modules. Parse extraction inputs at their AST/source owner, retain skipped HTML and source-map coverage, and distinguish endpoint candidates from proven receiver facts. Version structural fingerprints with computed literal contents and propagate them through worker contracts.

Reviewed fixes include #1810, #1840, #2642, #2700, #2702, #2714, #2803, #2960, #2968, #2986, #3035, and #3036.

Validation: 158 resolver tests passed; public CLI/MCP module lookup controls, package-main/Unicode workflows, and six builtin workflows passed in the current compiled lane. Worker schema/fingerprint regressions passed. Large public pdf.js completion and real Electron analysis remain unverified.
2026-10-14 08:46:50 +08:00

209 lines
7.2 KiB
TypeScript

import type { WebPageInspection } from "./browserObservationSchemas.js";
import {
webBundleAnalysisSchema,
type WebBundleAnalysis,
} from "./webBundleAnalysis.js";
import type {
AnalysisAccumulator,
IncludedScript,
} from "./webBundleAnalyzerInspection.js";
export const buildWebBundleAnalysis = (
inspection: WebPageInspection,
sourceScripts: IncludedScript[],
sourceMaps: WebBundleAnalysis["observations"]["source_maps"],
accumulator: AnalysisAccumulator,
): WebBundleAnalysis => {
const unavailable = inspection.scripts.items
.filter((script) => !script.source.included)
.map(({ script_key }) => script_key);
const omittedScripts = captureBudgetOmission(inspection, "scripts");
const omittedSourceMaps =
sourceMaps.status === "not_requested"
? undefined
: captureBudgetOmission(inspection, "source_maps");
const sourceMapIncomplete =
sourceMaps.status === "partial" || sourceMaps.status === "unavailable";
const partial =
accumulator.failedScripts.length > 0 ||
accumulator.recoveredScripts.length > 0 ||
unavailable.length > 0 ||
sourceMapIncomplete ||
omittedScripts !== undefined ||
omittedSourceMaps !== undefined;
return webBundleAnalysisSchema.parse({
capture: buildCaptureObservation(inspection, sourceScripts),
observations: {
chunks: {
nodes: sourceScripts.map(chunkNode),
edges: accumulator.edges,
},
routes: accumulator.routes,
endpoints: accumulator.endpoints,
webmcp_declarations: accumulator.webMcp,
source_maps: sourceMaps,
},
inferences: accumulator.inferences,
unknowns: buildUnknowns({
unavailable,
failedScripts: accumulator.failedScripts,
recoveredScripts: accumulator.recoveredScripts,
sourceMaps,
omittedScripts,
omittedSourceMaps,
}),
completeness: buildCompleteness({
partial,
parsedScripts: accumulator.parsedScripts,
parseFailures: accumulator.failedScripts.length,
visitedNodes: accumulator.visitedNodes,
}),
limitations: bundleLimitations(),
});
};
const buildCaptureObservation = (
inspection: WebPageInspection,
sourceScripts: IncludedScript[],
) => ({
target_url: inspection.target.url,
scripts_observed: inspection.scripts.total,
scripts_analyzed: sourceScripts.length,
source_artifacts: sourceScripts.map(sourceArtifact),
});
const sourceArtifact = (script: IncludedScript) => {
if (!script.source.included) throw new TypeError("Filtered source changed");
const { text: _text, ...artifact } = script.source.artifact;
return { ...artifact, text_available: true };
};
const chunkNode = (script: IncludedScript) => {
if (!script.source.included) throw new TypeError("Filtered source changed");
return {
script_key: script.script_key,
url: script.url,
artifact_sha256: script.source.artifact.sha256,
bytes: script.source.artifact.bytes,
};
};
interface UnknownsInput {
readonly unavailable: string[];
readonly failedScripts: readonly string[];
readonly recoveredScripts: readonly string[];
readonly sourceMaps: WebBundleAnalysis["observations"]["source_maps"];
readonly omittedScripts: number | null | undefined;
readonly omittedSourceMaps: number | null | undefined;
}
const buildUnknowns = (input: UnknownsInput): WebBundleAnalysis["unknowns"] => [
...(input.omittedScripts === undefined
? []
: [
{
dimension: "script_inventory",
reason: captureBudgetReason("script records", input.omittedScripts),
affected_script_keys: [],
},
]),
...(input.unavailable.length === 0
? []
: [
{
dimension: "script_source" as const,
reason: "Source artifact was not captured",
affected_script_keys: input.unavailable,
},
]),
...(input.failedScripts.length === 0
? []
: [
{
dimension: "javascript_ast" as const,
reason: "One or more source artifacts could not be parsed",
affected_script_keys: [...input.failedScripts],
},
]),
...(input.recoveredScripts.length === 0
? []
: [
{
dimension: "javascript_ast" as const,
reason:
"The parser recovered from errors in one or more source artifacts; findings may be incomplete",
affected_script_keys: [...input.recoveredScripts],
},
]),
...(input.omittedSourceMaps === undefined
? []
: [
{
dimension: "source_maps" as const,
reason: captureBudgetReason(
"source-map declarations",
input.omittedSourceMaps,
),
affected_script_keys: [],
},
]),
...(input.sourceMaps.status === "not_requested" ||
input.sourceMaps.status === "included"
? []
: [
{
dimension: "source_maps" as const,
reason:
input.sourceMaps.limitation ??
"One or more requested source maps were unavailable or incomplete",
affected_script_keys: input.sourceMaps.items
.filter(({ status }) => status !== "included")
.map(({ script_key }) => script_key)
.sort(),
},
]),
];
const captureBudgetOmission = (
inspection: WebPageInspection,
section: "scripts" | "source_maps",
): number | null | undefined => {
const exclusions = inspection.completeness.excluded.filter(
(exclusion) =>
exclusion.section === section &&
exclusion.reason === "resource_budget_exhausted",
);
if (exclusions.length === 0) return undefined;
if (exclusions.some(({ count }) => count === null)) return null;
return exclusions.reduce((sum, { count }) => sum + (count ?? 0), 0);
};
const captureBudgetReason = (label: string, count: number | null): string =>
count === null
? `Capture omitted an unknown number of ${label} because its resource budget was exhausted.`
: `Capture omitted ${String(count)} ${label} because its resource budget was exhausted.`;
interface CompletenessInput {
readonly partial: boolean;
readonly parsedScripts: number;
readonly parseFailures: number;
readonly visitedNodes: number;
}
const buildCompleteness = (
input: CompletenessInput,
): WebBundleAnalysis["completeness"] => ({
status: input.partial ? "partial" : "complete",
parsed_scripts: input.parsedScripts,
parse_failures: input.parseFailures,
visited_ast_nodes: input.visitedNodes,
});
const bundleLimitations = (): WebBundleAnalysis["limitations"] => [
"Static bundle findings are observations or bounded inferences; REA does not execute captured JavaScript.",
"Bare string arguments of unbound HTTP-method-named calls are low-confidence endpoint candidates; form, translation, storage and other APIs can use the same syntax. URL-shaped findings still do not prove network execution.",
'Endpoint candidates are deduplicated by script, value and detector; basis retains only the first matching source coordinate. They are not a complete call-site inventory or a runtime occurrence count. completeness.status="complete" describes examination of captured source, not a call-site census.',
"String-built routes and endpoints, encrypted configuration, and server-side behavior may remain unknown.",
"Page-declared WebMCP metadata is untrusted and is never registered or invoked as an REA tool.",
];