fix(javascript): preserve semantic gaps for unparseable sources

Successful worker transport can carry an explicit failed parser status
with no semantic projection. Preserve that producer fact through the
shared source-availability interpretation used by full and incremental
graph construction, rather than dropping the source's semantic scope.

Retain the module when capacity permits, per-family incomplete-module
unknowns, and unknown omitted extents. Preserve existing operational
failure reasons and limits without classifying syntax as resource loss.
Document this coverage behavior.

Validation: regression-first run had 3 expected failures; all 22 selected
projection and public CLI source-selection/HTML cases passed after repair.
No JavaScript target execution or live external provider was involved.
This commit is contained in:
morluto committed 2026-10-24 08:58:22 +08:00
1 parent 54646e8dc4
commit d81d6b6707
6 files changed
+105 -6

No files matched your search

@@ -204,6 +204,13 @@ those missing bytes. New inventories preserve an ASAR companion declaration as
unavailable bytes belonged to a companion. See [MCP integrity handling](mcp-contracts.md#integrity-record-and-continue)
for the tool-result contract.
JavaScript whose syntax cannot be parsed retains unavailable semantic coverage
with `incomplete-module` unknowns for each semantic family and unknown omitted
node and relation extents. The source module is retained when node capacity
permits. A parser failure alone does not imply resource truncation or a named
resource limit. Decoding failures and deliberately
unselected sources retain the same explicit semantic coverage gap.
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
@@ -25,7 +25,10 @@ import type {
JavaScriptArtifactFile,
JavaScriptArtifactFileSet,
} from "../../domain/javascript/javascriptArtifactFiles.js";
import { javaScriptArtifactTextExclusion } from "../../domain/javascript/javascriptArtifactFiles.js";
import {
javaScriptArtifactTextExclusion,
javaScriptSemanticSourceExclusion,
} from "../../domain/javascript/javascriptArtifactFiles.js";
import type {
AnalyzedJavaScriptArtifactFile,
JavaScriptArtifactAnalysisFailure,
@@ -189,11 +192,17 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
},
}),
});
if (failure !== undefined && facts.projection === null)
const semanticUnavailableReason =
failure?.reason ??
javaScriptSemanticSourceExclusion(
file,
facts.javascript?.parse_status ?? null,
);
if (facts.projection === null && semanticUnavailableReason !== null)
projection.recordUnavailableFile(
file,
failure.reason,
failure.limits,
semanticUnavailableReason,
failure?.limits ?? null,
error instanceof AnalysisResourceConstraintError ||
error instanceof AnalysisTimeoutError
? "resource-limit"
@@ -20,7 +20,7 @@ import { sourceRangesEqual as rangesEqual } from "../../domain/javascript/javasc
import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import {
javaScriptArtifactTextExclusion,
javaScriptSemanticSourceExclusion,
type JavaScriptArtifactFile,
} from "../../domain/javascript/javascriptArtifactFiles.js";
import {
@@ -160,7 +160,10 @@ export const buildJavaScriptSemanticGraph = ({
analyzed.javascript?.electron.call_paths,
);
else {
const reason = javaScriptArtifactTextExclusion(analyzed.file);
const reason = javaScriptSemanticSourceExclusion(
analyzed.file,
analyzed.javascript?.parse_status ?? null,
);
if (reason !== null)
projection.recordUnavailableFile(
analyzed.file,
@@ -6,6 +6,8 @@ import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTyp
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
import { resolveJavaScriptSemanticEvidence } from "../../domain/javascript/javascriptSemanticGraph.js";
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
import { analyzeJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
const SHA256 = "a".repeat(64);
const GRAPH_ID = `jag_${"b".repeat(64)}`;
@@ -391,6 +393,56 @@ it("uses unavailable evidence when no source produced semantic IR", () => {
);
});
it("preserves an unparseable source identity and unknown semantic extents", () => {
const source = "export const broken = ;";
const analysis = analyzeJavaScriptArtifactFiles({
files: [
{
path: "broken.mjs",
container_sha256: SHA256,
sha256: SHA256,
bytes: Buffer.byteLength(source),
inventory_artifact_id: `art_${SHA256}`,
kind: "javascript",
unpacked: false,
text: { included: true, value: source },
},
],
unavailable_relevant_files: [],
containers: [],
text_bytes_read: Buffer.byteLength(source),
invalid_utf8_files: 0,
});
expect(analysis.files[0]?.javascript?.parse_status).toBe("failed");
const graph = buildJavaScriptSemanticGraph({
rootArtifactSha256: SHA256,
applicationGraph: { graph_id: GRAPH_ID, nodes: [] },
analysis,
});
const module = graph.nodes.find(
({ kind, identity }) =>
kind === "module" && identity.module_path === "broken.mjs",
);
expect(module).toBeDefined();
expect(graph.unknowns).toHaveLength(
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.length,
);
for (const unknown of graph.unknowns) {
expect(unknown).toMatchObject({
node_id: module?.node_id,
reason: "incomplete-module",
});
expect(unknown.detail).toContain("syntax could not be parsed");
}
expect(graph.coverage).toMatchObject({
status: "partial",
truncated: false,
omitted_nodes: null,
omitted_relations: null,
limits: [],
});
});
const emptyAnalysis = (): JavaScriptArtifactAnalysis => ({
jsonDocuments: new JavaScriptJsonDocuments(),
files: [],
@@ -1,6 +1,7 @@
import { z } from "zod";
import { digestSchema } from "../digests.js";
import type { JavaScriptSourceSelection } from "./javascriptSourceSelection.js";
import type { JavaScriptStaticAnalysis } from "./javascriptStaticAnalysisTypes.js";
import type {
ArtifactInventoryCoverage,
ArtifactOccurrence,
@@ -61,6 +62,20 @@ export const javaScriptArtifactTextExclusion = (
: "Source bytes are not valid UTF-8; JavaScript semantics were not analyzed.";
};
/** Source availability shared by semantic consumers of parsed artifact facts. */
export const javaScriptSemanticSourceExclusion = (
file: JavaScriptArtifactFile,
parseStatus: JavaScriptStaticAnalysis["parse_status"] | null,
): string | null => {
if (file.kind !== "javascript") return null;
return (
javaScriptArtifactTextExclusion(file) ??
(parseStatus === "failed"
? "JavaScript syntax could not be parsed; JavaScript semantics were not analyzed."
: null)
);
};
/**
* One inventoried regular file not backed by analyzed source bytes. Its inventory
* occurrence remains available even when content is opaque or unavailable.
@@ -115,6 +115,19 @@ cliTest.for([
detail.includes("not valid UTF-8"),
),
).toHaveLength(JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.length);
const unparsedModule = full.semantic_graph.nodes.find(
({ kind, identity }) =>
kind === "module" && identity.module_path === "vendor.js",
);
expect(unparsedModule).toBeDefined();
expect(
full.semantic_graph.unknowns.filter(
({ node_id, detail, reason }) =>
node_id === unparsedModule?.node_id &&
reason === "incomplete-module" &&
detail.includes("syntax could not be parsed"),
),
).toHaveLength(JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.length);
expect(full.semantic_graph.coverage).toMatchObject({
status: "partial",
truncated: false,