From f36893a1a204b57b2fc0a15aaeb62fa1465fc7ee Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:04:49 +0800 Subject: [PATCH] fix(javascript): retain unavailable HTML reference scopes --- docs/javascript-artifact-reconstruction.md | 6 ++ .../JavaScriptArtifactGraphStructure.ts | 7 ++ .../javascriptUnavailableHtmlCli.test.ts | 85 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 tests/acceptance/applications/javascriptUnavailableHtmlCli.test.ts diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index 0aa0a9de8b..88ab8c54f7 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -194,6 +194,12 @@ missing native/resource bytes as unknown. The occurrence keeps those missing bytes. See [MCP integrity handling](mcp-contracts.md#integrity-record-and-continue) for the tool-result contract. +HTML whose bytes are not valid UTF-8 retains its file observation and an unknown +`parse-html-script-references` scope with artifact-byte authority. Reference +discovery was unavailable; zero retained script links does not establish that +the unreadable HTML declared none. Readable HTML with no references has no such +gap, and readable external references retain their load edges. + ## Large results Use MCP `detail: "summary"` for a large application, then read selected views of diff --git a/src/application/javascript/JavaScriptArtifactGraphStructure.ts b/src/application/javascript/JavaScriptArtifactGraphStructure.ts index c7bd387d89..720e4ea419 100644 --- a/src/application/javascript/JavaScriptArtifactGraphStructure.ts +++ b/src/application/javascript/JavaScriptArtifactGraphStructure.ts @@ -246,6 +246,13 @@ export const addJavaScriptArtifactFiles = ( ), details: { analysis_status: analyzed.analysis_failure.status }, }); + else if (file.kind === "html" && !file.text.included) + addUnavailableStaticParseScope(context, { + file, + asset: target, + ...javascriptArtifactAnalysisScopes.html, + limitation: `HTML text could not be decoded: ${file.text.reason}.`, + }); else if ( file.kind === "javascript" && (analyzed.javascript === null || diff --git a/tests/acceptance/applications/javascriptUnavailableHtmlCli.test.ts b/tests/acceptance/applications/javascriptUnavailableHtmlCli.test.ts new file mode 100644 index 0000000000..357cd3ebc1 --- /dev/null +++ b/tests/acceptance/applications/javascriptUnavailableHtmlCli.test.ts @@ -0,0 +1,85 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; + +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +cliTest.for([ + "invalid-html", + "empty-html", + "reference-html", + "invalid-json", +] as const)( + "retains applicable unavailable HTML reference scope through CLI for %s", + async (testCase, { cli }) => { + const root = await createTestTempDirectory("rea-unavailable-html-"); + const reference = ''; + const html = + testCase === "invalid-html" + ? Buffer.concat([Buffer.from(reference), Buffer.from([0xff])]) + : testCase === "reference-html" + ? reference + : ""; + await writeFile( + join(root, "package.json"), + '{"name":"html-scope","main":"main.js"}', + ); + await writeFile(join(root, "main.js"), "module.exports = 42;"); + await writeFile(join(root, "index.html"), html); + await writeFile( + join(root, "data.json"), + testCase === "invalid-json" ? Buffer.from([0xff]) : "{}", + ); + const response = await cli.run({ + arguments: [ + "analyze-javascript-application", + root, + ...["package.json", "main.js", "index.html", "data.json"].flatMap( + (path) => ["--source-path", path], + ), + "--format", + "json", + ], + }); + expect(response.exitCode, response.stderr).toBe(0); + const result = javascriptApplicationAnalysisResultSchema.parse( + parseEvidence(response.json).normalized_result, + ); + const scopes = result.graph.nodes + .filter(({ kind }) => kind === "unknown") + .flatMap(({ observations }) => observations); + const htmlScopes = scopes.filter( + ({ properties }) => + properties.path === "index.html" && + properties.operation === "parse-html-script-references", + ); + expect(htmlScopes).toHaveLength(testCase === "invalid-html" ? 1 : 0); + if (testCase === "invalid-html") { + expect(htmlScopes[0]?.evidence).toMatchObject({ + authority: "artifact-bytes", + state: "unavailable", + }); + expect(htmlScopes[0]?.properties.limitation).toContain("invalid-utf8"); + expect(result.graph.coverage).toMatchObject({ + status: "partial", + truncated: false, + }); + } + expect( + scopes.filter( + ({ properties }) => + properties.path === "data.json" && + properties.operation === "parse-json-module", + ), + ).toHaveLength(testCase === "invalid-json" ? 1 : 0); + expect( + result.graph.edges.filter( + ({ relation, properties }) => + relation === "loads" && properties.script_path !== undefined, + ), + ).toHaveLength(testCase === "reference-html" ? 1 : 0); + }, +);