mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 19:05:45 +00:00
166 lines
5.2 KiB
TypeScript
166 lines
5.2 KiB
TypeScript
import type { WebPageInspection } from "./browserObservation.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 sourceMapIncomplete =
|
|
sourceMaps.status === "partial" || sourceMaps.status === "unavailable";
|
|
const partial =
|
|
accumulator.parseFailures > 0 ||
|
|
unavailable.length > 0 ||
|
|
sourceMapIncomplete;
|
|
const truncated =
|
|
accumulator.astLimitReached ||
|
|
accumulator.droppedFindings > 0 ||
|
|
sourceMaps.status === "truncated";
|
|
return webBundleAnalysisSchema.parse({
|
|
schema_version: 1,
|
|
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({
|
|
sourceScripts,
|
|
unavailable,
|
|
parseFailures: accumulator.parseFailures,
|
|
sourceMaps,
|
|
}),
|
|
completeness: buildCompleteness({
|
|
truncated,
|
|
partial,
|
|
parsedScripts: accumulator.parsedScripts,
|
|
parseFailures: accumulator.parseFailures,
|
|
visitedNodes: accumulator.visitedNodes,
|
|
droppedFindings: accumulator.droppedFindings,
|
|
}),
|
|
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 sourceScripts: IncludedScript[];
|
|
readonly unavailable: string[];
|
|
readonly parseFailures: number;
|
|
readonly sourceMaps: WebBundleAnalysis["observations"]["source_maps"];
|
|
}
|
|
|
|
const buildUnknowns = (input: UnknownsInput): WebBundleAnalysis["unknowns"] => [
|
|
...(input.unavailable.length === 0
|
|
? []
|
|
: [
|
|
{
|
|
dimension: "script_source" as const,
|
|
reason: "Source artifact was not captured within approved limits",
|
|
affected_script_keys: input.unavailable,
|
|
},
|
|
]),
|
|
...(input.parseFailures === 0
|
|
? []
|
|
: [
|
|
{
|
|
dimension: "javascript_ast" as const,
|
|
reason: "One or more source artifacts could not be parsed",
|
|
affected_script_keys: input.sourceScripts.map(
|
|
({ script_key }) => script_key,
|
|
),
|
|
},
|
|
]),
|
|
...(input.sourceMaps.status === "not_requested" ||
|
|
input.sourceMaps.status === "included"
|
|
? []
|
|
: [
|
|
{
|
|
dimension: "source_maps" as const,
|
|
reason:
|
|
input.sourceMaps.status === "truncated"
|
|
? "Source-map evidence was truncated by approved limits"
|
|
: "One or more requested source maps were unavailable or incomplete",
|
|
affected_script_keys: [
|
|
...new Set([
|
|
...input.sourceMaps.items
|
|
.filter(({ status }) => status !== "included")
|
|
.map(({ script_key }) => script_key),
|
|
...input.sourceMaps.dropped_script_keys,
|
|
]),
|
|
].sort(),
|
|
},
|
|
]),
|
|
];
|
|
|
|
interface CompletenessInput {
|
|
readonly truncated: boolean;
|
|
readonly partial: boolean;
|
|
readonly parsedScripts: number;
|
|
readonly parseFailures: number;
|
|
readonly visitedNodes: number;
|
|
readonly droppedFindings: number;
|
|
}
|
|
|
|
const buildCompleteness = (
|
|
input: CompletenessInput,
|
|
): WebBundleAnalysis["completeness"] => ({
|
|
status: input.truncated
|
|
? "truncated"
|
|
: input.partial
|
|
? "partial"
|
|
: "complete_within_limits",
|
|
parsed_scripts: input.parsedScripts,
|
|
parse_failures: input.parseFailures,
|
|
visited_ast_nodes: input.visitedNodes,
|
|
dropped_findings: input.droppedFindings,
|
|
});
|
|
|
|
const bundleLimitations = (): WebBundleAnalysis["limitations"] => [
|
|
"Static bundle findings are observations or bounded inferences; REA does not execute captured JavaScript.",
|
|
"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.",
|
|
];
|