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
2026-10-24 08:58:22 +08:00
parent 54646e8dc4
commit d81d6b6707
6 changed files with 105 additions and 6 deletions
@@ -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: [],