* fix(javascript): bound semantic node projection for resource safety analyze_javascript_application exhausted the heap or failed serialization on ordinary inputs. A small extracted tree crashed outright, and a larger application tree failed at both the default heap and an 8 GB heap. Cause: the semantic projection emits one node per AST expression, call site, binding and property slot, with no ceiling. A five-file tree already yields on the order of 170,000 nodes, and the whole result is serialized after the object graph is built and validated. Result ceilings were deliberately removed upstream, so nothing bounded this growth. Apply two resource-safety ceilings and report them through the graph's own coverage contract rather than dropping data silently: - SEMANTIC_GRAPH_NODE_CEILING bounds retained nodes per tree (100,000, roughly 300 MB serialized, below V8's 512 MB string limit). - SEMANTIC_GRAPH_FILE_NODE_CEILING bounds each source file (20,000) so one bundled library cannot starve the application's own modules. Exhausting either marks coverage truncated with a partial status and records the ceiling in coverage.limits. A small application stays complete and untruncated. Verified: a previously-crashing application tree now returns Evidence in under a minute, while a small tree stays complete. Existing tests and check:fast pass. * fix(javascript): report semantic graph truncation accurately --------- Co-authored-by: REA Contributor <contributor@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
252 lines
8.7 KiB
TypeScript
252 lines
8.7 KiB
TypeScript
import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
|
|
import type { JsonValue } from "../../domain/jsonValue.js";
|
|
import { javascriptDisplayText } from "../../domain/javascript/javascriptAstValues.js";
|
|
import {
|
|
createJavaScriptSemanticGraphNode,
|
|
createJavaScriptSemanticGraphRelation,
|
|
type JavaScriptSemanticGraphNode,
|
|
} from "../../domain/javascript/javascriptSemanticGraph.js";
|
|
import type { ApplicationGraphEvidence } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
|
import type {
|
|
JavaScriptSemanticGraphRelation,
|
|
JavaScriptSemanticGraphUnknown,
|
|
} from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
|
|
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
|
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
|
import {
|
|
inferredSemanticEvidence,
|
|
observedSemanticEvidence,
|
|
unavailableSemanticRootEvidence,
|
|
} from "./JavaScriptSemanticGraphEvidence.js";
|
|
|
|
/** Mutable local projection state hidden from graph callers. */
|
|
export interface SemanticGraphProjectionState {
|
|
readonly nodes: Map<string, JavaScriptSemanticGraphNode>;
|
|
readonly relations: Map<string, JavaScriptSemanticGraphRelation>;
|
|
readonly unknowns: Map<string, JavaScriptSemanticGraphUnknown>;
|
|
readonly roots: Set<string>;
|
|
readonly applicationNodeIdsByLocation: ReadonlyMap<string, readonly string[]>;
|
|
/**
|
|
* Remaining node budget for the file currently being projected.
|
|
*
|
|
* The projection emits one node per AST expression, call site, binding and
|
|
* property slot, which is unbounded in file size: one bundled vendor library
|
|
* measured 135,286 nodes on its own. `null` means unbounded; a number is the
|
|
* number of nodes this file may still add. Exhausting it stops node creation
|
|
* for that file and is reported through the graph's `coverage` fields.
|
|
*/
|
|
fileNodeBudget?: number | null;
|
|
/**
|
|
* Whether the file currently being projected lost any nodes to its budget.
|
|
*
|
|
* A file that exactly fills its share ends with a zero budget without
|
|
* dropping anything, so callers must consult this flag rather than the
|
|
* remaining budget to decide whether coverage is truncated.
|
|
*/
|
|
fileNodesDropped?: boolean;
|
|
}
|
|
|
|
/** Input for one exact artifact-version semantic node. */
|
|
export interface SemanticNodeConstructionInput {
|
|
readonly kind: JavaScriptSemanticGraphNode["kind"];
|
|
readonly roleKey: string;
|
|
readonly location: JavaScriptSourceRange | null;
|
|
readonly label: string | null;
|
|
readonly functionNodeId: string | null;
|
|
readonly properties?: Readonly<Record<string, JsonValue>>;
|
|
}
|
|
|
|
/** Input for one directed static semantic relationship. */
|
|
export interface SemanticRelationConstructionInput {
|
|
readonly source: JavaScriptSemanticGraphNode | undefined | null;
|
|
readonly target: JavaScriptSemanticGraphNode | undefined | null;
|
|
readonly relation: JavaScriptSemanticGraphRelation["relation"];
|
|
readonly resolution?: JavaScriptSemanticGraphRelation["resolution"];
|
|
readonly evidence?: ApplicationGraphEvidence;
|
|
readonly properties?: Readonly<Record<string, JsonValue>>;
|
|
}
|
|
|
|
/** Create empty projection state with structural application-node mappings. */
|
|
export const createSemanticGraphProjectionState = (
|
|
applicationGraph: Pick<JavaScriptApplicationGraph, "nodes">,
|
|
): SemanticGraphProjectionState => {
|
|
const index = indexApplicationNodes(applicationGraph.nodes);
|
|
return {
|
|
nodes: new Map(),
|
|
relations: new Map(),
|
|
unknowns: new Map(),
|
|
roots: new Set(),
|
|
applicationNodeIdsByLocation: index.identifiers,
|
|
};
|
|
};
|
|
|
|
/** Construct one canonical semantic node backed by an exact artifact file. */
|
|
export const constructSemanticGraphNode = (
|
|
file: JavaScriptArtifactFile,
|
|
input: SemanticNodeConstructionInput,
|
|
state: SemanticGraphProjectionState,
|
|
): JavaScriptSemanticGraphNode =>
|
|
createJavaScriptSemanticGraphNode({
|
|
kind: input.kind,
|
|
identity: {
|
|
artifact_sha256: file.sha256,
|
|
module_path: file.path,
|
|
source_range: input.location,
|
|
role_key: input.roleKey,
|
|
},
|
|
function_node_id: input.functionNodeId,
|
|
application_node_ids: matchingApplicationNodeIds(file, input, state),
|
|
label: input.label === null ? null : javascriptDisplayText(input.label),
|
|
properties: input.properties ?? {},
|
|
evidence: observedSemanticEvidence(file, input.location),
|
|
});
|
|
|
|
/** Retain one canonical semantic node. */
|
|
export const addSemanticGraphNode = (
|
|
state: SemanticGraphProjectionState,
|
|
node: JavaScriptSemanticGraphNode,
|
|
): JavaScriptSemanticGraphNode | null => {
|
|
const existing = state.nodes.get(node.node_id);
|
|
if (existing !== undefined) return existing;
|
|
const budget = state.fileNodeBudget;
|
|
if (typeof budget === "number") {
|
|
if (budget <= 0) {
|
|
state.fileNodesDropped = true;
|
|
return null;
|
|
}
|
|
state.fileNodeBudget = budget - 1;
|
|
}
|
|
state.nodes.set(node.node_id, node);
|
|
return node;
|
|
};
|
|
|
|
/** Retain one non-self semantic relationship. */
|
|
export const addSemanticGraphRelation = (
|
|
state: SemanticGraphProjectionState,
|
|
input: SemanticRelationConstructionInput,
|
|
): void => {
|
|
if (
|
|
input.source === undefined ||
|
|
input.source === null ||
|
|
input.target === undefined ||
|
|
input.target === null ||
|
|
input.source.node_id === input.target.node_id
|
|
)
|
|
return;
|
|
const value = createJavaScriptSemanticGraphRelation({
|
|
source_node_id: input.source.node_id,
|
|
target_node_id: input.target.node_id,
|
|
relation: input.relation,
|
|
resolution: input.resolution ?? "candidate",
|
|
properties: input.properties ?? {},
|
|
evidence: input.evidence ?? inferredSemanticEvidence(input.source),
|
|
});
|
|
if (state.relations.has(value.relation_id)) return;
|
|
state.relations.set(value.relation_id, value);
|
|
};
|
|
|
|
/** Retain one unresolved frontier. */
|
|
export const addSemanticGraphUnknown = (
|
|
state: SemanticGraphProjectionState,
|
|
unknown: JavaScriptSemanticGraphUnknown,
|
|
): void => {
|
|
if (state.unknowns.has(unknown.unknown_id)) return;
|
|
state.unknowns.set(unknown.unknown_id, unknown);
|
|
};
|
|
|
|
/** Add a truthful root when no source file could produce semantic IR. */
|
|
export const addSemanticFallbackRoot = (
|
|
rootArtifactSha256: string,
|
|
state: SemanticGraphProjectionState,
|
|
): void => {
|
|
const node = addSemanticGraphNode(
|
|
state,
|
|
createJavaScriptSemanticGraphNode({
|
|
kind: "module",
|
|
identity: {
|
|
artifact_sha256: rootArtifactSha256,
|
|
module_path: "unknown-semantic-root",
|
|
source_range: null,
|
|
role_key: "artifact-root",
|
|
},
|
|
function_node_id: null,
|
|
application_node_ids: [],
|
|
label: "unavailable semantic root",
|
|
properties: {},
|
|
evidence: unavailableSemanticRootEvidence(rootArtifactSha256),
|
|
}),
|
|
);
|
|
if (node !== null) state.roots.add(node.node_id);
|
|
};
|
|
|
|
const indexApplicationNodes = (
|
|
nodes: JavaScriptApplicationGraph["nodes"],
|
|
): {
|
|
readonly identifiers: ReadonlyMap<string, readonly string[]>;
|
|
} => {
|
|
const identifiers = new Map<string, Set<string>>();
|
|
for (const node of nodes) {
|
|
for (const observation of node.observations) {
|
|
const { artifact, location } = observation.evidence;
|
|
if (!artifact.available || !location.available) continue;
|
|
if (location.value.kind === "artifact-path") {
|
|
addApplicationNodeIdentifier(
|
|
identifiers,
|
|
applicationLocationKey(artifact.sha256, location.value.path, null),
|
|
node.node_id,
|
|
);
|
|
}
|
|
if (location.value.kind === "source-range") {
|
|
addApplicationNodeIdentifier(
|
|
identifiers,
|
|
applicationLocationKey(artifact.sha256, location.value.source, {
|
|
start: location.value.start,
|
|
end: location.value.end,
|
|
}),
|
|
node.node_id,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
const indexed = new Map(
|
|
[...identifiers].map(([key, values]) => {
|
|
const sorted = [...values].sort();
|
|
return [key, sorted];
|
|
}),
|
|
);
|
|
return { identifiers: indexed };
|
|
};
|
|
|
|
const matchingApplicationNodeIds = (
|
|
file: JavaScriptArtifactFile,
|
|
input: SemanticNodeConstructionInput,
|
|
state: SemanticGraphProjectionState,
|
|
): string[] => [
|
|
...(state.applicationNodeIdsByLocation.get(
|
|
applicationLocationKey(file.sha256, file.path, input.location),
|
|
) ?? []),
|
|
];
|
|
|
|
const addApplicationNodeIdentifier = (
|
|
identifiers: Map<string, Set<string>>,
|
|
key: string,
|
|
nodeId: string,
|
|
): void => {
|
|
const values = identifiers.get(key) ?? new Set<string>();
|
|
values.add(nodeId);
|
|
identifiers.set(key, values);
|
|
};
|
|
|
|
const applicationLocationKey = (
|
|
artifactSha256: string,
|
|
path: string,
|
|
range: JavaScriptSourceRange | null,
|
|
): string =>
|
|
[
|
|
artifactSha256,
|
|
path,
|
|
range === null
|
|
? "artifact-path"
|
|
: `${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}`,
|
|
].join("\u0000");
|