diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md
index 88d97864a9..7ec2febebb 100644
--- a/docs/javascript-artifact-reconstruction.md
+++ b/docs/javascript-artifact-reconstruction.md
@@ -1002,7 +1002,12 @@ reason in the renderer observations.
HTML script source ranges follow the HTML parser across LF, CRLF, and bare CR
line endings, preserving UTF-16 columns.
Inline JavaScript in local HTML, meaning classic or module `', []],
["", []],
["", []],
- ["", []],
+ ["", ["classic"]],
])("classifies inline JavaScript in %s", (text, expected) => {
expect(
htmlArtifactReferences(text).inlineScripts.map(
@@ -115,6 +115,65 @@ it.each([
).toEqual(expected);
});
+it.each([
+ ['src="ignored.js"', "classic"],
+ ['language="vbscript"', "classic"],
+ ['xlink:type="simple"', "classic"],
+ ['type=" Text/JavaScript "', "classic"],
+ ['type="MODULE"', "module"],
+ ['type=" module "', null],
+ ['type="application/json"', null],
+ ['type="text/javascript; charset=utf-8"', null],
+ ['href="external.js"', null],
+ ['href=""', null],
+ ['xlink:href="external.js"', null],
+] as const)("classifies SVG inline bodies using %s", (attributes, expected) => {
+ const result = htmlArtifactReferences(
+ ``,
+ );
+ expect(result.inlineScripts.map(({ scriptType }) => scriptType)).toEqual(
+ expected === null ? [] : [expected],
+ );
+ expect(result.inlineScriptCount).toBe(expected === null ? 0 : 1);
+});
+
+it.each([
+ ['href="external.js"', ["external.js"]],
+ ['xlink:href="external.js"', ["external.js"]],
+ ['href="first.js" xlink:href="second.js"', ["first.js"]],
+ ['xlink:href="second.js" href="first.js"', ["first.js"]],
+ ['href="" xlink:href="ignored.js"', []],
+ ['other:href="ignored.js"', []],
+ ['src="ignored.js"', []],
+])("extracts SVG script URL from %s", (attributes, paths) => {
+ const result = htmlArtifactReferences(
+ ``,
+ );
+ expect(result.scripts.map(({ scriptPath }) => scriptPath)).toEqual(paths);
+ expect(result.inlineScriptCount).toBe(0);
+});
+
+it("shares precise body coordinates and location limits across HTML and SVG", () => {
+ const prefix = '\n`).inlineScripts,
+ ).toEqual([
+ {
+ scriptType: "classic",
+ location: { start: { line: 2, column: 30 }, end: { line: 4, column: 0 } },
+ },
+ ]);
+ const repeated = "".repeat(60);
+ const result = htmlArtifactReferences(repeated);
+ expect(result.inlineScriptCount).toBe(120);
+ expect(result.inlineScripts).toHaveLength(HTML_INLINE_SCRIPT_LOCATION_LIMIT);
+ expect(
+ htmlArtifactReferences("")
+ .inlineScriptCount,
+ ).toBe(0);
+});
+
it("retains exact inline script body coordinates, including unclosed scripts", () => {
const prefix = "\n`,
+ '',
+ '',
+ '',
+ '',
+ "",
+ ].join("\n");
+ await writeFile(join(root, "index.html"), html);
+ await writeFile(
+ join(root, "main.js"),
+ 'fetch("https://example.test/external");',
+ );
+ const response = await cli.run({
+ arguments: [
+ "analyze-javascript-application",
+ root,
+ "--source-path",
+ "index.html",
+ "--source-path",
+ "main.js",
+ "--format",
+ "json",
+ ],
+ });
+ expect(response.exitCode, response.stderr).toBe(0);
+ const result = javascriptApplicationAnalysisResultSchema.parse(
+ parseEvidence(response.json).normalized_result,
+ );
+ const observations = result.graph.nodes.flatMap(
+ ({ observations }) => observations,
+ );
+ const scopes = observations.filter(
+ ({ properties }) => properties.operation === "parse-html-inline-scripts",
+ );
+ expect(scopes).toHaveLength(1);
+ expect(scopes[0]).toMatchObject({
+ properties: {
+ path: "index.html",
+ inline_script_count: 1,
+ inline_scripts: [
+ {
+ script_type: "classic",
+ location: {
+ start: { line: 2, column: 45 },
+ end: { line: 2, column: 86 },
+ },
+ },
+ ],
+ },
+ evidence: {
+ state: "unavailable",
+ confidence: "unknown",
+ coverage: { status: "partial" },
+ },
+ });
+ expect(
+ result.graph.edges.filter(
+ ({ relation, properties }) =>
+ relation === "loads" && properties.resolved_path === "main.js",
+ ),
+ ).toHaveLength(2);
+ const endpoints = result.graph.nodes
+ .filter(({ kind }) => kind === "endpoint")
+ .flatMap(({ observations }) => observations)
+ .map(({ properties }) => properties.value);
+ expect(endpoints).toContain("https://example.test/external");
+ expect(endpoints).not.toContain(inlineUrl);
+ },
+);
+
cliTest.for([
"invalid-html",
"empty-html",