fix(browser): preserve script-specific parser uncertainty (#2858)

This commit is contained in:
xingzhou 2026-10-13 23:22:59 +08:00 committed by GitHub
parent 40e94aeb9d
commit e6c6fdd21a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 68 additions and 13 deletions

View File

@ -8,6 +8,48 @@ import { createWebTextArtifact } from "./webContentArtifact.js";
const origin = "https://app.example.test";
it.each([
{
kind: "recovered",
source: "function legacy(a, a) { return a; } fetch('/partial');",
parsed: 2,
failures: 0,
reason: "recovered",
endpoints: ["/partial", "/complete"],
},
{
kind: "failed",
source: "function broken( {",
parsed: 1,
failures: 1,
reason: "could not be parsed",
endpoints: ["/complete"],
},
])(
"attributes $kind parse uncertainty only to its affected source",
({ source, parsed, failures, reason, endpoints }) => {
const captured = inspection(source);
const good = inspection("fetch('/complete');").scripts.items[0];
if (good === undefined) throw new Error("Missing valid script fixture");
captured.scripts.items.push({ ...good, script_key: "valid-script" });
captured.scripts.total += 1;
const result = analyzeCapturedWebBundle(captured);
expect(result.completeness).toMatchObject({
status: "partial",
parsed_scripts: parsed,
parse_failures: failures,
});
expect(result.unknowns).toContainEqual({
dimension: "javascript_ast",
reason: expect.stringContaining(reason),
affected_script_keys: [captured.scripts.items[0]?.script_key],
});
expect(result.observations.endpoints.map(({ value }) => value)).toEqual(
endpoints,
);
},
);
describe("web bundle analyzer", () => {
it("extracts graph, route, endpoint, vendor, and WebMCP evidence", () => {
const source = `

View File

@ -50,7 +50,8 @@ export interface AnalysisAccumulator {
readonly seen: Set<string>;
visitedNodes: number;
parsedScripts: number;
parseFailures: number;
readonly failedScripts: string[];
readonly recoveredScripts: string[];
}
export const analyzeScript = (
@ -67,10 +68,12 @@ export const analyzeScript = (
} catch (cause: unknown) {
// Unparseable scripts are counted; the failure needs no extra detail.
void cause;
accumulator.parseFailures += 1;
accumulator.failedScripts.push(script.script_key);
return;
}
accumulator.parsedScripts += 1;
if ((file.errors?.length ?? 0) > 0)
accumulator.recoveredScripts.push(script.script_key);
detectVendorFingerprints(script, accumulator);
const openReceiverFacts = classifyParsedJavaScriptOpenReceivers(file);
const rpc = createJavaScriptRpcScan(
@ -386,7 +389,8 @@ export const emptyAccumulator = (): AnalysisAccumulator => ({
seen: new Set(),
visitedNodes: 0,
parsedScripts: 0,
parseFailures: 0,
failedScripts: [],
recoveredScripts: [],
});
export const isIncludedScript = (

View File

@ -25,7 +25,8 @@ export const buildWebBundleAnalysis = (
const sourceMapIncomplete =
sourceMaps.status === "partial" || sourceMaps.status === "unavailable";
const partial =
accumulator.parseFailures > 0 ||
accumulator.failedScripts.length > 0 ||
accumulator.recoveredScripts.length > 0 ||
unavailable.length > 0 ||
sourceMapIncomplete ||
omittedScripts !== undefined ||
@ -44,9 +45,9 @@ export const buildWebBundleAnalysis = (
},
inferences: accumulator.inferences,
unknowns: buildUnknowns({
sourceScripts,
unavailable,
parseFailures: accumulator.parseFailures,
failedScripts: accumulator.failedScripts,
recoveredScripts: accumulator.recoveredScripts,
sourceMaps,
omittedScripts,
omittedSourceMaps,
@ -54,7 +55,7 @@ export const buildWebBundleAnalysis = (
completeness: buildCompleteness({
partial,
parsedScripts: accumulator.parsedScripts,
parseFailures: accumulator.parseFailures,
parseFailures: accumulator.failedScripts.length,
visitedNodes: accumulator.visitedNodes,
}),
limitations: bundleLimitations(),
@ -88,9 +89,9 @@ const chunkNode = (script: IncludedScript) => {
};
interface UnknownsInput {
readonly sourceScripts: IncludedScript[];
readonly unavailable: string[];
readonly parseFailures: number;
readonly failedScripts: readonly string[];
readonly recoveredScripts: readonly string[];
readonly sourceMaps: WebBundleAnalysis["observations"]["source_maps"];
readonly omittedScripts: number | null | undefined;
readonly omittedSourceMaps: number | null | undefined;
@ -115,15 +116,23 @@ const buildUnknowns = (input: UnknownsInput): WebBundleAnalysis["unknowns"] => [
affected_script_keys: input.unavailable,
},
]),
...(input.parseFailures === 0
...(input.failedScripts.length === 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,
),
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